From 74220dd37ccc8f46f7b32ec20a1730f82f51a145 Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Wed, 26 Apr 2023 18:09:33 +0300 Subject: [PATCH 01/26] Fix downloading CourseSequential blocks (#21) --- .../Presentation/Container/CourseContainerViewModel.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index ca3150bdb..ac682d449 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -115,11 +115,9 @@ public class CourseContainerViewModel: BaseCourseViewModel { func onDownloadViewTap(chapter: CourseChapter, blockId: String, state: DownloadViewState) { let blocks = chapter.childs - .filter { $0.isDownloadable } + .first(where: { $0.id == blockId })?.childs .flatMap { $0.childs } - .filter { $0.isDownloadable } - .flatMap { $0.childs } - .filter { $0.isDownloadable } + .filter { $0.isDownloadable } ?? [] do { switch state { From 37e6615b6d73bb3815c39aa70a38e11a93bc93dd Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Wed, 10 May 2023 15:04:41 +0300 Subject: [PATCH 02/26] fix minor bugs (#22) * reverse sort years in edit profile * remove prints --- .../Details/CourseDetailsView.swift | 56 +++++++++++++------ .../Presentation/Unit/CourseUnitView.swift | 1 + .../CreateNewThreadViewModel.swift | 6 +- .../Presentation/Posts/PostsView.swift | 13 +++-- .../Presentation/Posts/PostsViewModel.swift | 1 - NewEdX/Router.swift | 2 +- NewEdX/uk.lproj/languages.json | 2 +- .../EditProfile/EditProfileViewModel.swift | 13 ++--- 8 files changed, 59 insertions(+), 35 deletions(-) diff --git a/Course/Course/Presentation/Details/CourseDetailsView.swift b/Course/Course/Presentation/Details/CourseDetailsView.swift index 77457a601..e44a696ba 100644 --- a/Course/Course/Presentation/Details/CourseDetailsView.swift +++ b/Course/Course/Presentation/Details/CourseDetailsView.swift @@ -65,7 +65,7 @@ public struct CourseDetailsView: View { // MARK: - iPad if idiom == .pad && viewModel.isHorisontal { - HStack { + HStack(alignment: .top) { VStack(alignment: .leading) { // MARK: - Title and description @@ -82,6 +82,7 @@ public struct CourseDetailsView: View { CourseBannerView( courseDetails: courseDetails, proxy: proxy, + isHorisontal: viewModel.isHorisontal, onPlayButtonTap: { [weak viewModel] in viewModel?.showCourseVideo() } @@ -95,28 +96,29 @@ public struct CourseDetailsView: View { } } else { // MARK: - iPhone - VStack { + VStack(alignment: .leading) { // MARK: - Course Banner CourseBannerView( courseDetails: courseDetails, proxy: proxy, + isHorisontal: viewModel.isHorisontal, onPlayButtonTap: { [weak viewModel] in viewModel?.showCourseVideo() }) }.aspectRatio(CGSize(width: 16, height: 8.5), contentMode: .fill) - .frame(maxHeight: 250) +// .frame(maxHeight: 250) .cornerRadius(12) .padding(.horizontal, 6) .padding(.top, 7) .fixedSize(horizontal: false, vertical: true) + // MARK: - Title and description + CourseTitleView(courseDetails: courseDetails) + // MARK: - Course state button CourseStateView(title: title, courseDetails: courseDetails, viewModel: viewModel) - - // MARK: - Title and description - CourseTitleView(courseDetails: courseDetails) } // MARK: - HTML Embed @@ -262,6 +264,7 @@ private struct CourseTitleView: View { private struct CourseBannerView: View { @State private var animate = false + private var isHorisontal: Bool private let courseDetails: CourseDetails private let idiom: UIUserInterfaceIdiom private let proxy: GeometryProxy @@ -269,8 +272,10 @@ private struct CourseBannerView: View { init(courseDetails: CourseDetails, proxy: GeometryProxy, + isHorisontal: Bool, onPlayButtonTap: @escaping () -> Void) { self.courseDetails = courseDetails + self.isHorisontal = isHorisontal self.idiom = UIDevice.current.userInterfaceIdiom self.proxy = proxy self.onPlayButtonTap = onPlayButtonTap @@ -278,19 +283,36 @@ private struct CourseBannerView: View { var body: some View { ZStack(alignment: .center) { - KFImage(URL(string: courseDetails.courseBannerURL)) - .onFailureImage(CoreAssets.noCourseImage.image) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: idiom == .pad ? 312 : proxy.size.width - 12) - .opacity(animate ? 1 : 0) - .onAppear { - withAnimation(.linear(duration: 0.5)) { - animate = true + if !isHorisontal { + KFImage(URL(string: courseDetails.courseBannerURL)) + .onFailureImage(CoreAssets.noCourseImage.image) + .resizable() + .aspectRatio(16/9, contentMode: .fill) + .frame(width: idiom == .pad ? nil : proxy.size.width - 12) + .opacity(animate ? 1 : 0) + .onAppear { + withAnimation(.linear(duration: 0.5)) { + animate = true + } + } + if courseDetails.courseVideoURL != nil { + PlayButton(action: onPlayButtonTap) + } + } else { + KFImage(URL(string: courseDetails.courseBannerURL)) + .onFailureImage(CoreAssets.noCourseImage.image) + .resizable() + .aspectRatio(16/9, contentMode: .fill) + .frame(width: idiom == .pad ? 312 : proxy.size.width - 12) + .opacity(animate ? 1 : 0) + .onAppear { + withAnimation(.linear(duration: 0.5)) { + animate = true + } } + if courseDetails.courseVideoURL != nil { + PlayButton(action: onPlayButtonTap) } - if courseDetails.courseVideoURL != nil { - PlayButton(action: onPlayButtonTap) } } } diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 0dbf62682..4b57e5945 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -108,6 +108,7 @@ public struct CourseUnitView: View { let id = "course-v1:" + (viewModel.lessonID.find(from: "block-v1:", to: "+type").first ?? "") PostsView(courseID: id, + currentBlockID: blockID, topics: Topics(coursewareTopics: [], nonCoursewareTopics: []), title: "", type: .courseTopics(topicID: blockID), diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift index 129199e85..a2f13a40c 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift @@ -44,8 +44,10 @@ public class CreateNewThreadViewModel: ObservableObject { if let topics { allTopics = topics.nonCoursewareTopics.map { $0 } allTopics.append(contentsOf: topics.coursewareTopics.flatMap { $0.children.map { $0 } }) - if let topic = allTopics.first { - selectedTopic = topic.id + if selectedTopic == "" { + if let topic = allTopics.first { + selectedTopic = topic.id + } } } } catch { diff --git a/Discussion/Discussion/Presentation/Posts/PostsView.swift b/Discussion/Discussion/Presentation/Posts/PostsView.swift index 838b00cd6..1ee9a943b 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsView.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsView.swift @@ -17,13 +17,15 @@ public struct PostsView: View { @State private var listAnimation: Animation? private let router: DiscussionRouter private let title: String + private let currentBlockID: String private let courseID: String private var showTopMenu: Bool - public init(courseID: String, topics: Topics, title: String, type: ThreadType, + public init(courseID: String, currentBlockID: String, topics: Topics, title: String, type: ThreadType, viewModel: PostsViewModel, router: DiscussionRouter, showTopMenu: Bool = true) { self.courseID = courseID self.title = title + self.currentBlockID = currentBlockID self.router = router self.showTopMenu = showTopMenu self.viewModel = viewModel @@ -31,17 +33,18 @@ public struct PostsView: View { self.viewModel.topics = topics viewModel.type = type Task { - await viewModel.getPosts(courseID: courseID, pageNumber: 1, withProgress: true) + await viewModel.getPosts(courseID: courseID, pageNumber: 1, withProgress: true) } } public init(courseID: String, router: DiscussionRouter, viewModel: PostsViewModel) { self.courseID = courseID self.title = "" + self.currentBlockID = "" self.router = router self.viewModel = viewModel Task { - await viewModel.getPosts(courseID: courseID, pageNumber: 1, withProgress: true) + await viewModel.getPosts(courseID: courseID, pageNumber: 1, withProgress: true) } self.showTopMenu = true self.viewModel.courseID = courseID @@ -137,7 +140,7 @@ public struct PostsView: View { Spacer() Button(action: { router.createNewThread(courseID: courseID, - selectedTopic: title, + selectedTopic: currentBlockID, onPostCreated: { reloadPage(onSuccess: { withAnimation { @@ -210,6 +213,7 @@ struct PostsView_Previews: PreviewProvider { ) PostsView(courseID: "course_id", + currentBlockID: "123", topics: topics, title: "Lesson question", type: .allPosts, @@ -220,6 +224,7 @@ struct PostsView_Previews: PreviewProvider { .loadFonts() PostsView(courseID: "course_id", + currentBlockID: "123", topics: topics, title: "Lesson question", type: .allPosts, diff --git a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift index f4a20a45b..3287b0ee1 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift @@ -159,7 +159,6 @@ public class PostsViewModel: ObservableObject { guard let self, let actualThread = self.threads.threads .first(where: {$0.id == thread.id }) else { return } - print(">>>>>", actualThread) self.router.showThread(thread: actualThread, postStateSubject: self.postStateSubject) })) } diff --git a/NewEdX/Router.swift b/NewEdX/Router.swift index a4c7d0b67..cfb51c2e2 100644 --- a/NewEdX/Router.swift +++ b/NewEdX/Router.swift @@ -224,7 +224,7 @@ public class Router: AuthorizationRouter, DiscoveryRouter, ProfileRouter, Dashbo public func showThreads(courseID: String, topics: Topics, title: String, type: ThreadType) { let router = Container.shared.resolve(DiscussionRouter.self)! let viewModel = Container.shared.resolve(PostsViewModel.self)! - let view = PostsView(courseID: courseID, topics: topics, title: title, + let view = PostsView(courseID: courseID, currentBlockID: "", topics: topics, title: title, type: type, viewModel: viewModel, router: router) let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) diff --git a/NewEdX/uk.lproj/languages.json b/NewEdX/uk.lproj/languages.json index 15d873ff6..5c07f7b8c 100644 --- a/NewEdX/uk.lproj/languages.json +++ b/NewEdX/uk.lproj/languages.json @@ -27,7 +27,7 @@ "hy": "Вірменська" }, { - "as": "Сссамська" + "as": "Саамська" }, { "av": "Аварська" diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift index ff5adb785..901874745 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift @@ -172,12 +172,6 @@ public class EditProfileViewModel: ObservableObject { } } } - if userModel.yearOfBirth == 0 { - withAnimation { - isYongUser = true - profileChanges.profileType = .limited - } - } if profileChanges.profileType == .full { isEditable = true } else { @@ -277,7 +271,7 @@ public class EditProfileViewModel: ObservableObject { private func generateYears() { let currentYear = Calendar.current.component(.year, from: Date()) years = [] - for i in currentYear-100...currentYear { + for i in stride(from: currentYear, to: currentYear - 100, by: -1) { years.append(PickerFields.Option(value: "\(i)", name: "\(i)", optionDefault: false)) } } @@ -321,8 +315,9 @@ public class EditProfileViewModel: ObservableObject { } public func loadLocationsAndSpokenLanguages() { - let yearOfBirth = userModel.yearOfBirth == 0 ? 2023 : userModel.yearOfBirth - self.selectedYearOfBirth = PickerItem(key: "\(yearOfBirth)", value: "\(yearOfBirth)") + if let yearOfBirth = userModel.yearOfBirth == 0 ? nil : userModel.yearOfBirth { + self.selectedYearOfBirth = PickerItem(key: "\(yearOfBirth)", value: "\(yearOfBirth)") + } if let index = countries.firstIndex(where: {$0.value == userModel.country}) { countries[index].optionDefault = true From d714e2c6ba952e51dc311c2f491be9555f21b22a Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Wed, 10 May 2023 15:10:16 +0300 Subject: [PATCH 03/26] add localization "ALERT.KEEP_EDITING" (#23) --- Core/Core/uk.lproj/Localizable.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/Core/uk.lproj/Localizable.strings b/Core/Core/uk.lproj/Localizable.strings index 939524031..430249a1d 100644 --- a/Core/Core/uk.lproj/Localizable.strings +++ b/Core/Core/uk.lproj/Localizable.strings @@ -30,6 +30,7 @@ "ALERT.CANCEL" = "СКАСУВАТИ"; "ALERT.LOGOUT" = "Вийти"; "ALERT.LEAVE" = "Покинути"; +"ALERT.KEEP_EDITING" = "Залишитись"; "NO_INTERNET.OFFLINE" = "Офлайн режим"; "NO_INTERNET.DISMISS" = "Сховати"; From 529cab2cf59b2a5fba10cad2d5b2d2c85aaff611 Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Fri, 12 May 2023 13:06:38 +0300 Subject: [PATCH 04/26] =?UTF-8?q?fix=20On=20the=20registration=20screen,?= =?UTF-8?q?=20screen=20title=20isn=E2=80=99t=20displayed=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Registration/SignUpView.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index a39ae94c6..725a49a86 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -35,8 +35,8 @@ public struct SignUpView: View { VStack(alignment: .center) { ZStack { HStack { - VStack {} - .titleSettings() + Text(AuthLocalization.SignUp.title) + .titleSettings(color: .white) } VStack { Button(action: { viewModel.router.back() }, label: { @@ -154,10 +154,12 @@ struct SignUpView_Previews: PreviewProvider { SignUpView(viewModel: vm) .preferredColorScheme(.light) .previewDisplayName("SignUpView Light") + .loadFonts() SignUpView(viewModel: vm) .preferredColorScheme(.dark) .previewDisplayName("SignUpView Dark") + .loadFonts() } } #endif From 9f099ed9977764aaea79fda977ddc05a2ed4750c Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Fri, 12 May 2023 13:07:14 +0300 Subject: [PATCH 05/26] fix Incorrect styles for video player (#25) --- Course/Course/Presentation/Video/EncodedVideoPlayer.swift | 2 ++ Course/Course/Presentation/Video/YouTubeVideoPlayer.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index 0a63cb913..30766bcc1 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -71,6 +71,8 @@ public struct EncodedVideoPlayer: View { currentTime = seconds }) .aspectRatio(16 / 9, contentMode: .fit) + .cornerRadius(12) + .padding(.horizontal, 6) .onReceive(NotificationCenter.Publisher( center: .default, name: UIDevice.orientationDidChangeNotification)) { _ in diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index dc80ddbfc..d5ffad4f9 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -95,6 +95,8 @@ public struct YouTubeVideoPlayer: View { } } }) + .cornerRadius(12) + .padding(.horizontal, 6) .aspectRatio(16/8.8, contentMode: .fit) .onReceive(NotificationCenter .Publisher(center: .default, From ff40906d44739093bb53e51b681449ef35421e4a Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Fri, 12 May 2023 13:08:00 +0300 Subject: [PATCH 06/26] fix Incorect icons for course components are shown (#26) --- .../discussion.imageset/Contents.json | 4 ++-- .../discussion.imageset/Frame-22.svg | 11 ---------- .../discussion.imageset/Frame-23.svg | 11 ---------- .../discussion.imageset/discussion.svg | 10 +++++++++ .../discussion.imageset/discussionBlack.svg | 10 +++++++++ .../finished.imageset/Contents.json | 22 +++++++++++++++++++ .../finished.imageset/checkDark.svg | 11 ++++++++++ .../finished.imageset/checkLight.svg | 11 ++++++++++ .../Core/Domain/Model/CourseDetailBlock.swift | 9 ++++---- Core/Core/SwiftGen/Assets.swift | 1 + .../Outline/CourseBlocksView.swift | 3 +-- 11 files changed, 73 insertions(+), 30 deletions(-) delete mode 100644 Core/Core/Assets.xcassets/Discussions/discussion.imageset/Frame-22.svg delete mode 100644 Core/Core/Assets.xcassets/Discussions/discussion.imageset/Frame-23.svg create mode 100644 Core/Core/Assets.xcassets/Discussions/discussion.imageset/discussion.svg create mode 100644 Core/Core/Assets.xcassets/Discussions/discussion.imageset/discussionBlack.svg create mode 100644 Core/Core/Assets.xcassets/Discussions/finished.imageset/Contents.json create mode 100644 Core/Core/Assets.xcassets/Discussions/finished.imageset/checkDark.svg create mode 100644 Core/Core/Assets.xcassets/Discussions/finished.imageset/checkLight.svg diff --git a/Core/Core/Assets.xcassets/Discussions/discussion.imageset/Contents.json b/Core/Core/Assets.xcassets/Discussions/discussion.imageset/Contents.json index 484e2073b..f4a247408 100644 --- a/Core/Core/Assets.xcassets/Discussions/discussion.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/Discussions/discussion.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Frame-23.svg", + "filename" : "discussionBlack.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "Frame-22.svg", + "filename" : "discussion.svg", "idiom" : "universal" } ], diff --git a/Core/Core/Assets.xcassets/Discussions/discussion.imageset/Frame-22.svg b/Core/Core/Assets.xcassets/Discussions/discussion.imageset/Frame-22.svg deleted file mode 100644 index c6c9bf80f..000000000 --- a/Core/Core/Assets.xcassets/Discussions/discussion.imageset/Frame-22.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/Discussions/discussion.imageset/Frame-23.svg b/Core/Core/Assets.xcassets/Discussions/discussion.imageset/Frame-23.svg deleted file mode 100644 index 16b49b29c..000000000 --- a/Core/Core/Assets.xcassets/Discussions/discussion.imageset/Frame-23.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/Discussions/discussion.imageset/discussion.svg b/Core/Core/Assets.xcassets/Discussions/discussion.imageset/discussion.svg new file mode 100644 index 000000000..114054921 --- /dev/null +++ b/Core/Core/Assets.xcassets/Discussions/discussion.imageset/discussion.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/Discussions/discussion.imageset/discussionBlack.svg b/Core/Core/Assets.xcassets/Discussions/discussion.imageset/discussionBlack.svg new file mode 100644 index 000000000..294b287a3 --- /dev/null +++ b/Core/Core/Assets.xcassets/Discussions/discussion.imageset/discussionBlack.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/Discussions/finished.imageset/Contents.json b/Core/Core/Assets.xcassets/Discussions/finished.imageset/Contents.json new file mode 100644 index 000000000..e1f0ce130 --- /dev/null +++ b/Core/Core/Assets.xcassets/Discussions/finished.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "checkLight.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "checkDark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Discussions/finished.imageset/checkDark.svg b/Core/Core/Assets.xcassets/Discussions/finished.imageset/checkDark.svg new file mode 100644 index 000000000..f81143453 --- /dev/null +++ b/Core/Core/Assets.xcassets/Discussions/finished.imageset/checkDark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/Discussions/finished.imageset/checkLight.svg b/Core/Core/Assets.xcassets/Discussions/finished.imageset/checkLight.svg new file mode 100644 index 000000000..cad0ffe27 --- /dev/null +++ b/Core/Core/Assets.xcassets/Discussions/finished.imageset/checkLight.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Core/Core/Domain/Model/CourseDetailBlock.swift b/Core/Core/Domain/Model/CourseDetailBlock.swift index 9b063eae5..279ec57cd 100644 --- a/Core/Core/Domain/Model/CourseDetailBlock.swift +++ b/Core/Core/Domain/Model/CourseDetailBlock.swift @@ -45,11 +45,12 @@ public enum BlockType: String { switch self { case .problem: return CoreAssets.extra.swiftUIImage.renderingMode(.template) case .video: return CoreAssets.video.swiftUIImage.renderingMode(.template) - case .html: return CoreAssets.chapter.swiftUIImage.renderingMode(.template) + case .html: return CoreAssets.pen.swiftUIImage.renderingMode(.template) case .discussion: return CoreAssets.discussion.swiftUIImage.renderingMode(.template) - case .course: return CoreAssets.extra.swiftUIImage.renderingMode(.template) - case .chapter: return CoreAssets.chapter.swiftUIImage.renderingMode(.template) - case .sequential: return CoreAssets.pen.swiftUIImage.renderingMode(.template) + case .course: return CoreAssets.pen.swiftUIImage.renderingMode(.template) + case .chapter: return CoreAssets.pen.swiftUIImage.renderingMode(.template) + case .sequential: return CoreAssets.chapter.swiftUIImage.renderingMode(.template) + case .vertical: return CoreAssets.pen.swiftUIImage.renderingMode(.template) default: return CoreAssets.extra.swiftUIImage.renderingMode(.template) } } diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index 829d65a8f..cbb63d5b6 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -58,6 +58,7 @@ public enum CoreAssets { public static let discussion = ImageAsset(name: "discussion") public static let extra = ImageAsset(name: "extra") public static let filter = ImageAsset(name: "filter") + public static let finished = ImageAsset(name: "finished") public static let followed = ImageAsset(name: "followed") public static let pen = ImageAsset(name: "pen") public static let question = ImageAsset(name: "question") diff --git a/Course/Course/Presentation/Outline/CourseBlocksView.swift b/Course/Course/Presentation/Outline/CourseBlocksView.swift index 0e5175fcc..5113d5983 100644 --- a/Course/Course/Presentation/Outline/CourseBlocksView.swift +++ b/Course/Course/Presentation/Outline/CourseBlocksView.swift @@ -45,8 +45,7 @@ public struct CourseBlocksView: View { HStack { Group { if block.completion == 1 { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) + CoreAssets.finished.swiftUIImage } else { block.type.image } From 67bb56136ae11d8edc3df450f42539bcec4fd74c Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Fri, 12 May 2023 13:58:10 +0300 Subject: [PATCH 07/26] fix Incorrect color for info message about screen rotation (#27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix Incorrect color for info message about screen rotation * fix Course component titles aren’t cropped by three dots * Fix video players --------- Co-authored-by: Volodymyr Chekyrta --- .../SnackbarInfoAlert.colorset/Contents.json | 38 +++++ Core/Core/SwiftGen/Assets.swift | 1 + Core/Core/View/Base/CourseButton.swift | 3 +- .../Outline/CourseBlocksView.swift | 145 ++++++++++-------- .../Outline/CourseOutlineView.swift | 6 + .../Outline/CourseVerticalView.swift | 135 ++++++++-------- .../Unit/CourseNavigationView.swift | 20 +-- .../Presentation/Unit/CourseUnitView.swift | 12 +- .../Unit/CourseUnitViewModel.swift | 1 - .../Video/EncodedVideoPlayer.swift | 3 +- .../Video/YouTubeVideoPlayer.swift | 2 +- 11 files changed, 217 insertions(+), 149 deletions(-) create mode 100644 Core/Core/Assets.xcassets/Colors/Snackbar/SnackbarInfoAlert.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/Snackbar/SnackbarInfoAlert.colorset/Contents.json b/Core/Core/Assets.xcassets/Colors/Snackbar/SnackbarInfoAlert.colorset/Contents.json new file mode 100644 index 000000000..3e35599d4 --- /dev/null +++ b/Core/Core/Assets.xcassets/Colors/Snackbar/SnackbarInfoAlert.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.667", + "red" : "0.259" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.584", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index cbb63d5b6..e9d8dfc49 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -38,6 +38,7 @@ public enum CoreAssets { public static let shadowColor = ColorAsset(name: "ShadowColor") public static let snackbarErrorColor = ColorAsset(name: "SnackbarErrorColor") public static let snackbarErrorTextColor = ColorAsset(name: "SnackbarErrorTextColor") + public static let snackbarInfoAlert = ColorAsset(name: "SnackbarInfoAlert") public static let styledButtonBackground = ColorAsset(name: "StyledButtonBackground") public static let styledButtonText = ColorAsset(name: "StyledButtonText") public static let textPrimary = ColorAsset(name: "TextPrimary") diff --git a/Core/Core/View/Base/CourseButton.swift b/Core/Core/View/Base/CourseButton.swift index d91b570d4..c3a607e35 100644 --- a/Core/Core/View/Base/CourseButton.swift +++ b/Core/Core/View/Base/CourseButton.swift @@ -26,7 +26,8 @@ public struct CourseButton: View { public var body: some View { HStack { if isCompleted { - Image(systemName: "checkmark.circle.fill") + CoreAssets.finished.swiftUIImage + .renderingMode(.template) .foregroundColor(.accentColor) } else { image diff --git a/Course/Course/Presentation/Outline/CourseBlocksView.swift b/Course/Course/Presentation/Outline/CourseBlocksView.swift index 5113d5983..bf44cd0bb 100644 --- a/Course/Course/Presentation/Outline/CourseBlocksView.swift +++ b/Course/Course/Presentation/Outline/CourseBlocksView.swift @@ -15,7 +15,8 @@ public struct CourseBlocksView: View { private var title: String @ObservedObject private var viewModel: CourseBlocksViewModel - + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + public init(title: String, viewModel: CourseBlocksViewModel) { self.title = title @@ -26,78 +27,86 @@ public struct CourseBlocksView: View { ZStack(alignment: .top) { // MARK: - Page name - VStack(alignment: .center) { - NavigationBar(title: title, - leftButtonAction: { viewModel.router.back() }) - - // MARK: - Page Body - ScrollView { - VStack(alignment: .leading) { - // MARK: - Lessons list - ForEach(viewModel.blocks, id: \.id) { block in - let index = viewModel.blocks.firstIndex(where: { $0.id == block.id }) - Button(action: { - viewModel.router.showCourseUnit(blockId: block.id, - courseID: block.blockId, - sectionName: title, - blocks: viewModel.blocks) - }, label: { - HStack { - Group { - if block.completion == 1 { - CoreAssets.finished.swiftUIImage - } else { - block.type.image - } - Text(block.displayName) - .multilineTextAlignment(.leading) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) - Spacer() - if let state = viewModel.downloadState[block.id] { - switch state { - case .available: - DownloadAvailableView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: block.id, state: state) - } - .onForeground { - viewModel.onForeground() - } - case .downloading: - DownloadProgressView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: block.id, state: state) - } - .onBackground { - viewModel.onBackground() - } - case .finished: - DownloadFinishedView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: block.id, state: state) - } + GeometryReader { proxy in + VStack(alignment: .center) { + NavigationBar(title: title, + leftButtonAction: { viewModel.router.back() }) + + // MARK: - Page Body + ScrollView { + VStack(alignment: .leading) { + // MARK: - Lessons list + ForEach(viewModel.blocks, id: \.id) { block in + let index = viewModel.blocks.firstIndex(where: { $0.id == block.id }) + Button(action: { + viewModel.router.showCourseUnit(blockId: block.id, + courseID: block.blockId, + sectionName: title, + blocks: viewModel.blocks) + }, label: { + HStack { + Group { + if block.completion == 1 { + CoreAssets.finished.swiftUIImage + .renderingMode(.template) + .foregroundColor(.accentColor) + } else { + block.type.image + } + Text(block.displayName) + .multilineTextAlignment(.leading) + .font(Theme.Fonts.titleMedium) + .lineLimit(1) + .frame(maxWidth: idiom == .pad + ? proxy.size.width * 0.5 + : proxy.size.width * 0.6, + alignment: .leading) + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) + Spacer() + if let state = viewModel.downloadState[block.id] { + switch state { + case .available: + DownloadAvailableView() + .onTapGesture { + viewModel.onDownloadViewTap(blockId: block.id, state: state) + } + .onForeground { + viewModel.onForeground() + } + case .downloading: + DownloadProgressView() + .onTapGesture { + viewModel.onDownloadViewTap(blockId: block.id, state: state) + } + .onBackground { + viewModel.onBackground() + } + case .finished: + DownloadFinishedView() + .onTapGesture { + viewModel.onDownloadViewTap(blockId: block.id, state: state) + } + } } + Image(systemName: "chevron.right") + .padding(.vertical, 8) } - Image(systemName: "chevron.right") - .padding(.vertical, 8) + }).padding(.horizontal, 36) + .padding(.vertical, 14) + if index != viewModel.blocks.count - 1 { + Divider() + .frame(height: 1) + .overlay(CoreAssets.cardViewStroke.swiftUIColor) + .padding(.horizontal, 24) } - }).padding(.horizontal, 36) - .padding(.vertical, 14) - if index != viewModel.blocks.count - 1 { - Divider() - .frame(height: 1) - .overlay(CoreAssets.cardViewStroke.swiftUIColor) - .padding(.horizontal, 24) } } - } - Spacer(minLength: 84) - }.frameLimit() - .onRightSwipeGesture { - viewModel.router.back() - } + Spacer(minLength: 84) + }.frameLimit() + .onRightSwipeGesture { + viewModel.router.back() + } + } } // MARK: - Offline mode SnackBar diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 3a26579b6..59ba3e669 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -17,6 +17,7 @@ public struct CourseOutlineView: View { private let isVideo: Bool @State private var openCertificateView: Bool = false + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } public init( viewModel: CourseContainerViewModel, @@ -117,6 +118,11 @@ public struct CourseOutlineView: View { Text(child.displayName) .font(Theme.Fonts.titleMedium) .multilineTextAlignment(.leading) + .lineLimit(1) + .frame(maxWidth: idiom == .pad + ? proxy.size.width * 0.5 + : proxy.size.width * 0.6, + alignment: .leading) }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() if let state = viewModel.downloadState[child.id] { diff --git a/Course/Course/Presentation/Outline/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVerticalView.swift index be44690b6..f68e7b314 100644 --- a/Course/Course/Presentation/Outline/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVerticalView.swift @@ -15,6 +15,7 @@ public struct CourseVerticalView: View { private var title: String @ObservedObject private var viewModel: CourseVerticalViewModel + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } public init( title: String, @@ -31,74 +32,82 @@ public struct CourseVerticalView: View { leftButtonAction: { viewModel.router.back() }) // MARK: - Page Body - ScrollView { - VStack(alignment: .leading) { - // MARK: - Lessons list - ForEach(viewModel.verticals, id: \.id) { vertical in - let index = viewModel.verticals.firstIndex(where: {$0.id == vertical.id}) - Button(action: { - viewModel.router.showCourseBlocksView( - title: vertical.displayName, - blocks: vertical.childs - ) - }, label: { - HStack { - Group { - if vertical.completion == 1 { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) - } else { - vertical.type.image - } - Text(vertical.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) - Spacer() - if let state = viewModel.downloadState[vertical.id] { - switch state { - case .available: - DownloadAvailableView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: vertical.id, state: state) - } - .onForeground { - viewModel.onForeground() - } - case .downloading: - DownloadProgressView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: vertical.id, state: state) - } - .onBackground { - viewModel.onBackground() - } - case .finished: - DownloadFinishedView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: vertical.id, state: state) - } + GeometryReader { proxy in + ScrollView { + VStack(alignment: .leading) { + // MARK: - Lessons list + ForEach(viewModel.verticals, id: \.id) { vertical in + let index = viewModel.verticals.firstIndex(where: {$0.id == vertical.id}) + Button(action: { + viewModel.router.showCourseBlocksView( + title: vertical.displayName, + blocks: vertical.childs + ) + }, label: { + HStack { + Group { + if vertical.completion == 1 { + CoreAssets.finished.swiftUIImage + .renderingMode(.template) + .foregroundColor(.accentColor) + } else { + vertical.type.image + } + Text(vertical.displayName) + .font(Theme.Fonts.titleMedium) + .lineLimit(1) + .frame(maxWidth: idiom == .pad + ? proxy.size.width * 0.5 + : proxy.size.width * 0.6, + alignment: .leading) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) + Spacer() + if let state = viewModel.downloadState[vertical.id] { + switch state { + case .available: + DownloadAvailableView() + .onTapGesture { + viewModel.onDownloadViewTap(blockId: vertical.id, state: state) + } + .onForeground { + viewModel.onForeground() + } + case .downloading: + DownloadProgressView() + .onTapGesture { + viewModel.onDownloadViewTap(blockId: vertical.id, state: state) + } + .onBackground { + viewModel.onBackground() + } + case .finished: + DownloadFinishedView() + .onTapGesture { + viewModel.onDownloadViewTap(blockId: vertical.id, state: state) + } + } } + Image(systemName: "chevron.right") + .padding(.vertical, 8) } - Image(systemName: "chevron.right") - .padding(.vertical, 8) + }).padding(.horizontal, 36) + .padding(.vertical, 14) + if index != viewModel.verticals.count - 1 { + Divider() + .frame(height: 1) + .overlay(CoreAssets.cardViewStroke.swiftUIColor) + .padding(.horizontal, 24) } - }).padding(.horizontal, 36) - .padding(.vertical, 14) - if index != viewModel.verticals.count - 1 { - Divider() - .frame(height: 1) - .overlay(CoreAssets.cardViewStroke.swiftUIColor) - .padding(.horizontal, 24) } } - } - Spacer(minLength: 84) - }.frameLimit() - .onRightSwipeGesture { - viewModel.router.back() - } + Spacer(minLength: 84) + }.frameLimit() + .onRightSwipeGesture { + viewModel.router.back() + } + } } // MARK: - Offline mode SnackBar diff --git a/Course/Course/Presentation/Unit/CourseNavigationView.swift b/Course/Course/Presentation/Unit/CourseNavigationView.swift index 106a05173..9692e14fe 100644 --- a/Course/Course/Presentation/Unit/CourseNavigationView.swift +++ b/Course/Course/Presentation/Unit/CourseNavigationView.swift @@ -12,11 +12,12 @@ struct CourseNavigationView: View { @ObservedObject private var viewModel: CourseUnitViewModel private let sectionName: String + @Binding var killPlayer: Bool - init(sectionName: String, viewModel: CourseUnitViewModel) { + init(sectionName: String, viewModel: CourseUnitViewModel, killPlayer: Binding) { self.viewModel = viewModel self.sectionName = sectionName - + self._killPlayer = killPlayer } var body: some View { @@ -24,24 +25,24 @@ struct CourseNavigationView: View { if viewModel.selectedLesson() == viewModel.blocks.first && viewModel.blocks.count != 1 { UnitButtonView(type: .first, action: { + killPlayer.toggle() viewModel.select(move: .next) - self.viewModel.createLessonType() - self.viewModel.killPlayer.toggle() + viewModel.createLessonType() }) } else { if viewModel.previousLesson != "" { UnitButtonView(type: .previous, action: { + killPlayer.toggle() viewModel.select(move: .previous) - self.viewModel.createLessonType() - self.viewModel.killPlayer.toggle() + viewModel.createLessonType() }) } if viewModel.nextLesson != "" { UnitButtonView(type: .next, action: { + killPlayer.toggle() viewModel.select(move: .next) - self.viewModel.createLessonType() - self.viewModel.killPlayer.toggle() + viewModel.createLessonType() }) } if viewModel.selectedLesson() == viewModel.blocks.last { @@ -54,6 +55,7 @@ struct CourseNavigationView: View { image: CoreAssets.goodWork.swiftUIImage, onCloseTapped: {}, okTapped: { + killPlayer.toggle() viewModel.router.dismiss(animated: false) viewModel.router.removeLastView(controllers: 2) } @@ -78,7 +80,7 @@ struct CourseNavigationView_Previews: PreviewProvider { connectivity: Connectivity(), manager: DownloadManagerMock()) - CourseNavigationView(sectionName: "Name", viewModel: viewModel) + CourseNavigationView(sectionName: "Name", viewModel: viewModel, killPlayer: .constant(false)) } } #endif diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 4b57e5945..92af45d6c 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -22,6 +22,7 @@ public struct CourseUnitView: View { } } } + @State var killPlayer: Bool = false private let sectionName: String public init(viewModel: CourseUnitViewModel, @@ -41,7 +42,7 @@ public struct CourseUnitView: View { NavigationBar(title: "", leftButtonAction: { viewModel.router.back() - self.viewModel.killPlayer.toggle() + killPlayer.toggle() }) // MARK: - Page Body @@ -72,7 +73,7 @@ public struct CourseUnitView: View { blockID: blockID, courseID: viewModel.courseID, languages: viewModel.languages(), - killPlayer: $viewModel.killPlayer + killPlayer: $killPlayer ) Spacer() case .web(let url): @@ -133,7 +134,7 @@ public struct CourseUnitView: View { .padding(.horizontal, 20) UnitButtonView(type: .reload, action: { self.viewModel.createLessonType() - self.viewModel.killPlayer.toggle() + killPlayer.toggle() }).frame(width: 100) }.frame(maxWidth: .infinity, maxHeight: .infinity) } @@ -163,7 +164,8 @@ public struct CourseUnitView: View { // MARK: - Course Navigation CourseNavigationView( sectionName: sectionName, - viewModel: viewModel + viewModel: viewModel, + killPlayer: $killPlayer ).padding(.vertical, 12) .frameLimit(sizePortrait: 420) .background( @@ -176,7 +178,7 @@ public struct CourseUnitView: View { }.frame(maxWidth: .infinity) .onRightSwipeGesture { viewModel.router.back() - self.viewModel.killPlayer.toggle() + killPlayer.toggle() } } diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index 9c4cf1b01..2d859ae5c 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -47,7 +47,6 @@ public class CourseUnitViewModel: ObservableObject { @Published var previousLesson: String = "" @Published var nextLesson: String = "" @Published var lessonType: LessonType? - @Published var killPlayer = false @Published var showError: Bool = false var errorMessage: String? { didSet { diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index 30766bcc1..0eaaad718 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -100,6 +100,7 @@ public struct EncodedVideoPlayer: View { } } }.onChange(of: killPlayer, perform: { _ in + controller.player?.pause() controller.player?.replaceCurrentItem(with: nil) }) // MARK: - Alert @@ -109,7 +110,7 @@ public struct EncodedVideoPlayer: View { HStack(spacing: 6) { CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template) Text(alertMessage ?? "") - }.shadowCardStyle(bgColor: CoreAssets.accentColor.swiftUIColor, + }.shadowCardStyle(bgColor: CoreAssets.snackbarInfoAlert.swiftUIColor, textColor: .white) .transition(.move(edge: .bottom)) .onAppear { diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index d5ffad4f9..3662ba357 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -162,7 +162,7 @@ public struct YouTubeVideoPlayer: View { HStack(spacing: 6) { CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template) Text(alertMessage ?? "") - }.shadowCardStyle(bgColor: CoreAssets.accentColor.swiftUIColor, + }.shadowCardStyle(bgColor: CoreAssets.snackbarInfoAlert.swiftUIColor, textColor: .white) .transition(.move(edge: .bottom)) .onAppear { From c99c28c5a323d0066f62dbfe7a6bee6d4fec16b2 Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Fri, 19 May 2023 11:43:12 +0300 Subject: [PATCH 08/26] Fix course icons (#28) --- Core/Core/Domain/Model/CourseDetailBlock.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Core/Domain/Model/CourseDetailBlock.swift b/Core/Core/Domain/Model/CourseDetailBlock.swift index 279ec57cd..219a0d4ea 100644 --- a/Core/Core/Domain/Model/CourseDetailBlock.swift +++ b/Core/Core/Domain/Model/CourseDetailBlock.swift @@ -43,9 +43,9 @@ public enum BlockType: String { public var image: Image { switch self { - case .problem: return CoreAssets.extra.swiftUIImage.renderingMode(.template) + case .problem: return CoreAssets.pen.swiftUIImage.renderingMode(.template) case .video: return CoreAssets.video.swiftUIImage.renderingMode(.template) - case .html: return CoreAssets.pen.swiftUIImage.renderingMode(.template) + case .html: return CoreAssets.extra.swiftUIImage.renderingMode(.template) case .discussion: return CoreAssets.discussion.swiftUIImage.renderingMode(.template) case .course: return CoreAssets.pen.swiftUIImage.renderingMode(.template) case .chapter: return CoreAssets.pen.swiftUIImage.renderingMode(.template) From 25ea471b11eb0359c8021a743d48ca1bbd2239e8 Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Mon, 22 May 2023 18:13:55 +0300 Subject: [PATCH 09/26] MBS-9 & MBS-15 (#29) * MBS-9 & MBS-15 --- .../Presentation/Registration/SignUpView.swift | 2 +- Course/Course/Data/Model/Data_ResumeBlock.swift | 2 +- .../Container/CourseContainerViewModel.swift | 10 ++++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 725a49a86..7286086f0 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -35,7 +35,7 @@ public struct SignUpView: View { VStack(alignment: .center) { ZStack { HStack { - Text(AuthLocalization.SignUp.title) + Text(AuthLocalization.SignIn.registerBtn) .titleSettings(color: .white) } VStack { diff --git a/Course/Course/Data/Model/Data_ResumeBlock.swift b/Course/Course/Data/Model/Data_ResumeBlock.swift index 506defef3..c49167a1b 100644 --- a/Course/Course/Data/Model/Data_ResumeBlock.swift +++ b/Course/Course/Data/Model/Data_ResumeBlock.swift @@ -30,6 +30,6 @@ public extension DataLayer { public extension DataLayer.ResumeBlock { var domain: ResumeBlock { - ResumeBlock(blockID: lastVisitedModulePath.first ?? "") + ResumeBlock(blockID: lastVisitedBlockID) } } diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index ac682d449..4ad87108e 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -176,8 +176,14 @@ public class CourseContainerViewModel: BaseCourseViewModel { private func findCourseSequential(blockID: String, courseStructure: CourseStructure) -> CourseSequential? { for chapter in courseStructure.childs { - if let sequential = chapter.childs.first(where: { $0.id == blockID }) { - return sequential + for sequential in chapter.childs { + for vertical in sequential.childs { + for block in vertical.childs { + if block.id == blockID { + return sequential + } + } + } } } return nil From 13f46b89e8a86f38eea19bcc1c1a569a7fe17a55 Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Thu, 25 May 2023 14:10:18 +0300 Subject: [PATCH 10/26] Open external links in the browser (#30) --- Core/Core/View/Base/WebBrowser.swift | 2 +- Core/Core/View/Base/WebUnitView.swift | 8 +++++--- Core/Core/View/Base/WebUnitViewModel.swift | 6 +++++- Core/Core/View/Base/WebView.swift | 23 +++++++++++++++++++++- NewEdX/DI/ScreenAssembly.swift | 3 ++- 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift index 04ae19b99..1919ce173 100644 --- a/Core/Core/View/Base/WebBrowser.swift +++ b/Core/Core/View/Base/WebBrowser.swift @@ -31,7 +31,7 @@ public struct WebBrowser: View { VStack { ZStack(alignment: .top) { NavigationView { - WebView(viewModel: .init(url: url), isLoading: $isShowProgress, refreshCookies: {}) + WebView(viewModel: .init(url: url, baseURL: ""), isLoading: $isShowProgress, refreshCookies: {}) .navigationBarTitle(Text("")) // Needed for hide navBar on ios 14, 15 .navigationBarHidden(true) .ignoresSafeArea() diff --git a/Core/Core/View/Base/WebUnitView.swift b/Core/Core/View/Base/WebUnitView.swift index 12f6e162b..b9dc4e772 100644 --- a/Core/Core/View/Base/WebUnitView.swift +++ b/Core/Core/View/Base/WebUnitView.swift @@ -28,9 +28,11 @@ public struct WebUnitView: View { GeometryReader { reader in ScrollView { if viewModel.cookiesReady { - WebView(viewModel: .init(url: url), isLoading: $isWebViewLoading, refreshCookies: { - await viewModel.updateCookies(force: true) - }) + WebView( + viewModel: .init(url: url, baseURL: viewModel.config.baseURL.absoluteString), + isLoading: $isWebViewLoading, refreshCookies: { + await viewModel.updateCookies(force: true) + }) .introspectScrollView(customize: { scrollView in scrollView.isScrollEnabled = false scrollView.alwaysBounceVertical = false diff --git a/Core/Core/View/Base/WebUnitViewModel.swift b/Core/Core/View/Base/WebUnitViewModel.swift index 9b06a1daa..866f5dba4 100644 --- a/Core/Core/View/Base/WebUnitViewModel.swift +++ b/Core/Core/View/Base/WebUnitViewModel.swift @@ -9,7 +9,10 @@ import Foundation import SwiftUI public class WebUnitViewModel: ObservableObject { + let authInteractor: AuthInteractorProtocol + let config: Config + @Published var updatingCookies: Bool = false @Published var cookiesReady: Bool = false @Published var showError: Bool = false @@ -23,8 +26,9 @@ public class WebUnitViewModel: ObservableObject { } } - public init(authInteractor: AuthInteractorProtocol) { + public init(authInteractor: AuthInteractorProtocol, config: Config) { self.authInteractor = authInteractor + self.config = config } @MainActor diff --git a/Core/Core/View/Base/WebView.swift b/Core/Core/View/Base/WebView.swift index 14fb8da4e..4ad2f9d5c 100644 --- a/Core/Core/View/Base/WebView.swift +++ b/Core/Core/View/Base/WebView.swift @@ -12,10 +12,13 @@ import SwiftUI public struct WebView: UIViewRepresentable { public class ViewModel: ObservableObject { + @Published var url: String + let baseURL: String - public init(url: String) { + public init(url: String, baseURL: String) { self.url = url + self.baseURL = baseURL } } @@ -42,6 +45,24 @@ public struct WebView: UIViewRepresentable { } } + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { + + guard let url = navigationAction.request.url else { + return .cancel + } + + let baseURL = await parent.viewModel.baseURL + if !baseURL.isEmpty, !url.absoluteString.starts(with: baseURL) { + await MainActor.run { + UIApplication.shared.open(url, options: [:]) + } + return .cancel + } + + return .allow + } + public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy { guard let statusCode = (navigationResponse.response as? HTTPURLResponse)?.statusCode else { diff --git a/NewEdX/DI/ScreenAssembly.swift b/NewEdX/DI/ScreenAssembly.swift index f0636f596..373311ddc 100644 --- a/NewEdX/DI/ScreenAssembly.swift +++ b/NewEdX/DI/ScreenAssembly.swift @@ -244,7 +244,8 @@ class ScreenAssembly: Assembly { } container.register(WebUnitViewModel.self) { r in - WebUnitViewModel(authInteractor: r.resolve(AuthInteractorProtocol.self)!) + WebUnitViewModel(authInteractor: r.resolve(AuthInteractorProtocol.self)!, + config: r.resolve(Config.self)!) } container.register(VideoPlayerViewModel.self) { r in From d8b24e986f4b63c2e39bdcd35de551555f1dc270 Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta Date: Thu, 25 May 2023 17:40:06 +0300 Subject: [PATCH 11/26] Fix Discussion incorrect replies count --- Core/Core.xcodeproj/project.pbxproj | 4 +++ Core/Core/Data/Model/Data_Discovery.swift | 8 +++-- Core/Core/Domain/Model/Pagination.swift | 22 ++++++++++++ .../Data/Model/Data_CommentsResponse.swift | 34 +++++++++---------- .../Data/Network/DiscussionRepository.swift | 30 ++++++++-------- .../Domain/DiscussionInteractor.swift | 12 +++---- .../Base/BaseResponsesViewModel.swift | 2 ++ .../Comments/Responses/ResponsesView.swift | 8 ++--- .../Responses/ResponsesViewModel.swift | 5 +-- .../Comments/Thread/ThreadView.swift | 8 ++--- .../Comments/Thread/ThreadViewModel.swift | 13 ++++--- 11 files changed, 87 insertions(+), 59 deletions(-) create mode 100644 Core/Core/Domain/Model/Pagination.swift diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index f8b06176f..731ba83cb 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ 072787B628D37A0E002E9142 /* Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072787B528D37A0E002E9142 /* Validator.swift */; }; 07460FE1294B706200F70538 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07460FE0294B706200F70538 /* CollectionExtension.swift */; }; 07460FE3294B72D700F70538 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07460FE2294B72D700F70538 /* Notification.swift */; }; + 076F297F2A1F80C800967E7D /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076F297E2A1F80C800967E7D /* Pagination.swift */; }; 0770DE1928D0847D006D8A5D /* BaseRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE1828D0847D006D8A5D /* BaseRouter.swift */; }; 0770DE2528D08FBA006D8A5D /* AppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2428D08FBA006D8A5D /* AppStorage.swift */; }; 0770DE2A28D0929E006D8A5D /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2928D0929E006D8A5D /* HTTPTask.swift */; }; @@ -217,6 +218,7 @@ 07460FE0294B706200F70538 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; 07460FE2294B72D700F70538 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 0754BB7841E3C0F8D6464951 /* Pods-App-Core.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releasestage.xcconfig"; sourceTree = ""; }; + 076F297E2A1F80C800967E7D /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = ""; }; 0770DE0828D07831006D8A5D /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0770DE1828D0847D006D8A5D /* BaseRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseRouter.swift; sourceTree = ""; }; 0770DE2428D08FBA006D8A5D /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = ""; }; @@ -425,6 +427,7 @@ 027BD39B2908810C00392132 /* RegisterUser.swift */, 028F9F38293A452B00DE65D0 /* ResetPassword.swift */, 020C31C8290AC3F700D6DEA2 /* PickerFields.swift */, + 076F297E2A1F80C800967E7D /* Pagination.swift */, ); path = Model; sourceTree = ""; @@ -779,6 +782,7 @@ 027BD3AE2909475000392132 /* KeyboardScrollerOptions.swift in Sources */, 027BD3BE2909478B00392132 /* UIResponder+CurrentResponder.swift in Sources */, 070019AE28F701B200D5FC78 /* Certificate.swift in Sources */, + 076F297F2A1F80C800967E7D /* Pagination.swift in Sources */, 0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */, 02C917F029CDA99E00DBB8BD /* Data_Dashboard.swift in Sources */, 024FCD0028EF1CD300232339 /* WebBrowser.swift in Sources */, diff --git a/Core/Core/Data/Model/Data_Discovery.swift b/Core/Core/Data/Model/Data_Discovery.swift index e44786bc9..6bbc014b5 100644 --- a/Core/Core/Data/Model/Data_Discovery.swift +++ b/Core/Core/Data/Model/Data_Discovery.swift @@ -7,8 +7,6 @@ import Foundation -// MARK: "/api/courses/v1/courses/" - // MARK: - Pagination public extension DataLayer { struct Pagination: Codable { @@ -115,3 +113,9 @@ public extension DataLayer.DiscoveryResponce { return listReady } } + +public extension DataLayer.Pagination { + var domain: Pagination { + Pagination(next: next, previous: previous, count: count, numPages: numPages) + } +} diff --git a/Core/Core/Domain/Model/Pagination.swift b/Core/Core/Domain/Model/Pagination.swift new file mode 100644 index 000000000..9cbad7b18 --- /dev/null +++ b/Core/Core/Domain/Model/Pagination.swift @@ -0,0 +1,22 @@ +// +// Pagination.swift +// Core +// +// Created by Vladimir Chekyrta on 25.05.2023. +// + +import Foundation + +public struct Pagination { + public let next: String? + public let previous: String? + public let count: Int + public let numPages: Int + + public init(next: String?, previous: String?, count: Int, numPages: Int) { + self.next = next + self.previous = previous + self.count = count + self.numPages = numPages + } +} diff --git a/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift b/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift index d99831abb..d95cde415 100644 --- a/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift +++ b/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift @@ -105,23 +105,23 @@ public extension DataLayer { public extension DataLayer.CommentsResponse { var domain: [UserComment] { self.comments.map { comment in - UserComment( - authorName: comment.author ?? DiscussionLocalization.anonymous, - authorAvatar: comment.users?.userName?.profile?.image?.imageURLLarge ?? "", - postDate: Date(iso8601: comment.createdAt), - postTitle: "", - postBody: comment.rawBody, - postBodyHtml: comment.renderedBody, - postVisible: true, - voted: comment.voted, - followed: false, - votesCount: comment.voteCount, - responsesCount: pagination.count, - threadID: comment.threadID, - commentID: comment.id, - parentID: comment.id, - abuseFlagged: comment.abuseFlagged - ) + UserComment( + authorName: comment.author ?? DiscussionLocalization.anonymous, + authorAvatar: comment.users?.userName?.profile?.image?.imageURLLarge ?? "", + postDate: Date(iso8601: comment.createdAt), + postTitle: "", + postBody: comment.rawBody, + postBodyHtml: comment.renderedBody, + postVisible: true, + voted: comment.voted, + followed: false, + votesCount: comment.voteCount, + responsesCount: comment.childCount, + threadID: comment.threadID, + commentID: comment.id, + parentID: comment.id, + abuseFlagged: comment.abuseFlagged + ) } } } diff --git a/Discussion/Discussion/Data/Network/DiscussionRepository.swift b/Discussion/Discussion/Data/Network/DiscussionRepository.swift index db9b95627..d213ddb67 100644 --- a/Discussion/Discussion/Data/Network/DiscussionRepository.swift +++ b/Discussion/Discussion/Data/Network/DiscussionRepository.swift @@ -17,9 +17,9 @@ public protocol DiscussionRepositoryProtocol { page: Int) async throws -> ThreadLists func searchThreads(courseID: String, searchText: String, pageNumber: Int) async throws -> ThreadLists func getTopics(courseID: String) async throws -> Topics - func getDiscussionComments(threadID: String, page: Int) async throws -> ([UserComment], Int) - func getQuestionComments(threadID: String, page: Int) async throws -> ([UserComment], Int) - func getCommentResponses(commentID: String, page: Int) async throws -> ([UserComment], Int) + func getDiscussionComments(threadID: String, page: Int) async throws -> ([UserComment], Pagination) + func getQuestionComments(threadID: String, page: Int) async throws -> ([UserComment], Pagination) + func getCommentResponses(commentID: String, page: Int) async throws -> ([UserComment], Pagination) func addCommentTo(threadID: String, rawBody: String, parentID: String?) async throws -> Post func voteThread(voted: Bool, threadID: String) async throws func voteResponse(voted: Bool, responseID: String) async throws @@ -72,25 +72,25 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { return topics.domain } - public func getDiscussionComments(threadID: String, page: Int) async throws -> ([UserComment], Int) { + public func getDiscussionComments(threadID: String, page: Int) async throws -> ([UserComment], Pagination) { let response = try await api.requestData(DiscussionEndpoint .getDiscussionComments(threadID: threadID, page: page)) let result = try await renameUsers(data: response) - return (result.domain, result.pagination.numPages) + return (result.domain, result.pagination.domain) } - public func getQuestionComments(threadID: String, page: Int) async throws -> ([UserComment], Int) { + public func getQuestionComments(threadID: String, page: Int) async throws -> ([UserComment], Pagination) { let response = try await api.requestData(DiscussionEndpoint .getQuestionComments(threadID: threadID, page: page)) let result = try await renameUsers(data: response) - return (result.domain, result.pagination.numPages) + return (result.domain, result.pagination.domain) } - public func getCommentResponses(commentID: String, page: Int) async throws -> ([UserComment], Int) { + public func getCommentResponses(commentID: String, page: Int) async throws -> ([UserComment], Pagination) { let response = try await api.requestData(DiscussionEndpoint .getCommentResponses(commentID: commentID, page: page)) let result = try await renameUsers(data: response) - return (result.domain, result.pagination.numPages) + return (result.domain, result.pagination.domain) } public func addCommentTo(threadID: String, rawBody: String, parentID: String? = nil) async throws -> Post { @@ -327,16 +327,16 @@ public class DiscussionRepositoryMock: DiscussionRepositoryProtocol { ) } - public func getDiscussionComments(threadID: String, page: Int) async throws -> ([UserComment], Int) { - (comments, 10) + public func getDiscussionComments(threadID: String, page: Int) async throws -> ([UserComment], Pagination) { + (comments, Pagination(next: nil, previous: nil, count: 10, numPages: 1)) } - public func getQuestionComments(threadID: String, page: Int) async throws -> ([UserComment], Int) { - (comments, 10) + public func getQuestionComments(threadID: String, page: Int) async throws -> ([UserComment], Pagination) { + (comments, Pagination(next: nil, previous: nil, count: 10, numPages: 1)) } - public func getCommentResponses(commentID: String, page: Int) async throws -> ([UserComment], Int) { - (comments, 10) + public func getCommentResponses(commentID: String, page: Int) async throws -> ([UserComment], Pagination) { + (comments, Pagination(next: nil, previous: nil, count: 10, numPages: 1)) } public func addCommentTo(threadID: String, rawBody: String, parentID: String?) async throws -> Post { diff --git a/Discussion/Discussion/Domain/DiscussionInteractor.swift b/Discussion/Discussion/Domain/DiscussionInteractor.swift index d17043b03..ee6de6716 100644 --- a/Discussion/Discussion/Domain/DiscussionInteractor.swift +++ b/Discussion/Discussion/Domain/DiscussionInteractor.swift @@ -17,9 +17,9 @@ public protocol DiscussionInteractorProtocol { page: Int) async throws -> ThreadLists func getTopics(courseID: String) async throws -> Topics func searchThreads(courseID: String, searchText: String, pageNumber: Int) async throws -> ThreadLists - func getDiscussionComments(threadID: String, page: Int) async throws -> ([UserComment], Int) - func getQuestionComments(threadID: String, page: Int) async throws -> ([UserComment], Int) - func getCommentResponses(commentID: String, page: Int) async throws -> ([UserComment], Int) + func getDiscussionComments(threadID: String, page: Int) async throws -> ([UserComment], Pagination) + func getQuestionComments(threadID: String, page: Int) async throws -> ([UserComment], Pagination) + func getCommentResponses(commentID: String, page: Int) async throws -> ([UserComment], Pagination) func addCommentTo(threadID: String, rawBody: String, parentID: String?) async throws -> Post func voteThread(voted: Bool, threadID: String) async throws func voteResponse(voted: Bool, responseID: String) async throws @@ -54,15 +54,15 @@ public class DiscussionInteractor: DiscussionInteractorProtocol { return try await repository.getTopics(courseID: courseID) } - public func getDiscussionComments(threadID: String, page: Int) async throws -> ([UserComment], Int) { + public func getDiscussionComments(threadID: String, page: Int) async throws -> ([UserComment], Pagination) { return try await repository.getDiscussionComments(threadID: threadID, page: page) } - public func getQuestionComments(threadID: String, page: Int) async throws -> ([UserComment], Int) { + public func getQuestionComments(threadID: String, page: Int) async throws -> ([UserComment], Pagination) { return try await repository.getQuestionComments(threadID: threadID, page: page) } - public func getCommentResponses(commentID: String, page: Int) async throws -> ([UserComment], Int) { + public func getCommentResponses(commentID: String, page: Int) async throws -> ([UserComment], Pagination) { return try await repository.getCommentResponses(commentID: commentID, page: page) } diff --git a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift index 3a3694fc5..d7b7ce10d 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift @@ -22,6 +22,7 @@ public class BaseResponsesViewModel { public var nextPage = 2 public var totalPages = 1 + @Published public var itemsCount = 0 public var fetchInProgress = false var errorMessage: String? { @@ -138,6 +139,7 @@ public class BaseResponsesViewModel { var newPostWithAvatar = post newPostWithAvatar.authorAvatar = storage.userProfile?.profileImage?.imageURLLarge ?? "" postComments?.comments.append(newPostWithAvatar) + itemsCount += 1 } private func toggleLikeOnParrent() { diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift index 2e70cb9a2..7f8dde1b0 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift @@ -85,11 +85,9 @@ public struct ResponsesView: View { onFollowTap: {} ) HStack { - if let responsesCount = viewModel.postComments?.responsesCount { - Text("\(responsesCount)") - Text(DiscussionLocalization.commentsCount(responsesCount)) - Spacer() - } + Text("\(viewModel.itemsCount)") + Text(DiscussionLocalization.commentsCount(viewModel.itemsCount)) + Spacer() }.padding(.top, 40) .padding(.bottom, 14) .padding(.leading, 24) diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift index f3f377d14..b165565b9 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift @@ -92,9 +92,10 @@ public class ResponsesViewModel: BaseResponsesViewModel, ObservableObject { public func getComments(commentID: String, parentComment: Post, page: Int) async -> Bool { guard !fetchInProgress else { return false } do { - let (comments, totalPages) = try await interactor + let (comments, pagination) = try await interactor .getCommentResponses(commentID: commentID, page: page) - self.totalPages = totalPages + self.totalPages = pagination.numPages + self.itemsCount = pagination.count self.comments += comments postComments = generateCommentsResponses(comments: self.comments, parentComment: parentComment) return true diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index dc40a34f7..12513d490 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -96,11 +96,9 @@ public struct ThreadView: View { ) HStack { - if let responsesCount = viewModel.postComments?.responsesCount { - Text("\(responsesCount)") - Text(DiscussionLocalization.responsesCount(responsesCount)) - Spacer() - } + Text("\(viewModel.itemsCount)") + Text(DiscussionLocalization.responsesCount(viewModel.itemsCount)) + Spacer() }.padding(.top, 40) .padding(.bottom, 14) .padding(.leading, 24) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index a4ac75651..0457c8be0 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -128,20 +128,19 @@ public class ThreadViewModel: BaseResponsesViewModel, ObservableObject { try await interactor.readBody(threadID: thread.id) switch thread.type { case .question: - let (comments, totalPages) = try await interactor + let (comments, pagination) = try await interactor .getQuestionComments(threadID: thread.id, page: page) self.totalPages = totalPages + self.itemsCount = pagination.count self.comments += comments - - postComments = - generateComments(comments: self.comments, thread: thread) + postComments = generateComments(comments: self.comments, thread: thread) case .discussion: - let (comments, totalPages) = try await interactor + let (comments, pagination) = try await interactor .getDiscussionComments(threadID: thread.id, page: page) self.totalPages = totalPages + self.itemsCount = pagination.count self.comments += comments - postComments = - generateComments(comments: self.comments, thread: thread) + postComments = generateComments(comments: self.comments, thread: thread) } fetchInProgress = false return true From 71e7fc268850d100fb9adb6c8646eafdaa310165 Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Fri, 26 May 2023 17:25:04 +0300 Subject: [PATCH 12/26] Bugfix/mbs 25 tablet incorrect styles continue button (#32) * fix Incorrect styles Continue button on the course outline screen * wifi default state fixed --- Core/Core/Data/AppStorage.swift | 2 +- .../Outline/CourseOutlineView.swift | 68 ++++++++++++------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/Core/Core/Data/AppStorage.swift b/Core/Core/Data/AppStorage.swift index 7bd28811a..48f088070 100644 --- a/Core/Core/Data/AppStorage.swift +++ b/Core/Core/Data/AppStorage.swift @@ -79,7 +79,7 @@ public class AppStorage { public var userSettings: UserSettings? { get { guard let userSettings = userDefaults.data(forKey: KEY_SETTINGS) else { - let defaultSettings = UserSettings(wifiOnly: false, downloadQuality: .auto) + let defaultSettings = UserSettings(wifiOnly: true, downloadQuality: .auto) let encoder = JSONEncoder() if let encoded = try? encoder.encode(defaultSettings) { userDefaults.set(encoded, forKey: KEY_SETTINGS) diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 59ba3e669..8cc925146 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -231,32 +231,54 @@ public struct CourseOutlineView: View { struct ContinueWithView: View { let sequential: CourseSequential let viewModel: CourseContainerViewModel - + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + var body: some View { - VStack(alignment: .leading) { - if let vertical = sequential.childs.first { - Text(CourseLocalization.Courseware.continueWith) - .font(Theme.Fonts.labelMedium) - .foregroundColor(CoreAssets.textSecondary.swiftUIColor) - HStack { - vertical.type.image - Text(vertical.displayName) - .multilineTextAlignment(.leading) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) - UnitButtonView(type: .continueLesson, action: { -// viewModel.router.showCourseBlocksView(title: vertical.displayName, -// blocks: vertical.childs) -// viewModel.router.showCourseVerticalView(title: sequential.displayName, -// verticals: sequential.childs) - viewModel.router.showCourseVerticalAndBlocksView(verticals: (sequential.displayName, sequential.childs), - blocks: (vertical.displayName, vertical.childs)) - }) + if idiom == .pad { + HStack(alignment: .top) { + if let vertical = sequential.childs.first { + VStack(alignment: .leading) { + Text(CourseLocalization.Courseware.continueWith) + .font(Theme.Fonts.labelMedium) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) + HStack { + vertical.type.image + Text(vertical.displayName) + .multilineTextAlignment(.leading) + .font(Theme.Fonts.titleMedium) + .multilineTextAlignment(.leading) + } + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) + Spacer() + UnitButtonView(type: .continueLesson, action: { + viewModel.router.showCourseVerticalAndBlocksView(verticals: (sequential.displayName, sequential.childs), + blocks: (vertical.displayName, vertical.childs)) + }).frame(width: 200) + } + } .padding(.horizontal, 24) + .padding(.top, 32) + } else { + VStack(alignment: .leading) { + if let vertical = sequential.childs.first { + Text(CourseLocalization.Courseware.continueWith) + .font(Theme.Fonts.labelMedium) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) + HStack { + vertical.type.image + Text(vertical.displayName) + .multilineTextAlignment(.leading) + .font(Theme.Fonts.titleMedium) + .multilineTextAlignment(.leading) + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) + UnitButtonView(type: .continueLesson, action: { + viewModel.router.showCourseVerticalAndBlocksView(verticals: (sequential.displayName, sequential.childs), + blocks: (vertical.displayName, vertical.childs)) + }) + } } + .padding(.horizontal, 24) + .padding(.top, 32) } - .padding(.horizontal, 24) - .padding(.top, 32) } } From b805911ed4f42d349241b392fa5690a271086ba3 Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Wed, 31 May 2023 17:13:13 +0300 Subject: [PATCH 13/26] change unit button on course vertical (#33) --- Core/Core/Domain/Model/CourseDetailBlock.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Core/Domain/Model/CourseDetailBlock.swift b/Core/Core/Domain/Model/CourseDetailBlock.swift index 219a0d4ea..8230d44df 100644 --- a/Core/Core/Domain/Model/CourseDetailBlock.swift +++ b/Core/Core/Domain/Model/CourseDetailBlock.swift @@ -50,7 +50,7 @@ public enum BlockType: String { case .course: return CoreAssets.pen.swiftUIImage.renderingMode(.template) case .chapter: return CoreAssets.pen.swiftUIImage.renderingMode(.template) case .sequential: return CoreAssets.chapter.swiftUIImage.renderingMode(.template) - case .vertical: return CoreAssets.pen.swiftUIImage.renderingMode(.template) + case .vertical: return CoreAssets.extra.swiftUIImage.renderingMode(.template) default: return CoreAssets.extra.swiftUIImage.renderingMode(.template) } } From 6813ce34d45352fa5151bc10031efef434d88e4d Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Wed, 31 May 2023 17:14:16 +0300 Subject: [PATCH 14/26] fix "enroll to course" button position on the iPad (#34) --- Course/Course/Presentation/Details/CourseDetailsView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Course/Course/Presentation/Details/CourseDetailsView.swift b/Course/Course/Presentation/Details/CourseDetailsView.swift index e44a696ba..6d494854a 100644 --- a/Course/Course/Presentation/Details/CourseDetailsView.swift +++ b/Course/Course/Presentation/Details/CourseDetailsView.swift @@ -204,7 +204,6 @@ private struct CourseStateView: View { } }) .padding(16) - .frame(maxWidth: .infinity) case .enrollClose: Text(CourseLocalization.Details.enrollmentDateIsOver) .multilineTextAlignment(.center) From 6512acf7f0074aae319a63c8b9d4ffc7e3a0ff31 Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Wed, 31 May 2023 17:15:36 +0300 Subject: [PATCH 15/26] Show EmptyPageIcon() on DashboardView only after fetch is finished. (#35) --- Dashboard/Dashboard/Presentation/DashboardView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dashboard/Dashboard/Presentation/DashboardView.swift b/Dashboard/Dashboard/Presentation/DashboardView.swift index cc3430c26..d353148cf 100644 --- a/Dashboard/Dashboard/Presentation/DashboardView.swift +++ b/Dashboard/Dashboard/Presentation/DashboardView.swift @@ -48,7 +48,7 @@ public struct DashboardView: View { withProgress: isIOS14, refresh: true) }) { - if viewModel.courses.isEmpty { + if !viewModel.fetchInProgress || viewModel.courses.isEmpty { EmptyPageIcon() } else { LazyVStack(spacing: 0) { From 5649d5eb61737f008e7a6373f95111f44dd676b5 Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:39:19 +0300 Subject: [PATCH 16/26] Fix dashboard empty state (#36) --- Dashboard/Dashboard/Presentation/DashboardView.swift | 10 +++------- .../Dashboard/Presentation/DashboardViewModel.swift | 9 +++++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Dashboard/Dashboard/Presentation/DashboardView.swift b/Dashboard/Dashboard/Presentation/DashboardView.swift index d353148cf..78e3c802a 100644 --- a/Dashboard/Dashboard/Presentation/DashboardView.swift +++ b/Dashboard/Dashboard/Presentation/DashboardView.swift @@ -44,11 +44,9 @@ public struct DashboardView: View { ZStack { RefreshableScrollViewCompat(action: { - await viewModel.getMyCourses(page: 1, - withProgress: isIOS14, - refresh: true) + await viewModel.getMyCourses(page: 1, refresh: true) }) { - if !viewModel.fetchInProgress || viewModel.courses.isEmpty { + if viewModel.courses.isEmpty && !viewModel.fetchInProgress { EmptyPageIcon() } else { LazyVStack(spacing: 0) { @@ -104,9 +102,7 @@ public struct DashboardView: View { // MARK: - Offline mode SnackBar OfflineSnackBarView(connectivity: viewModel.connectivity, reloadAction: { - await viewModel.getMyCourses( page: 1, - withProgress: isIOS14, - refresh: true) + await viewModel.getMyCourses(page: 1, refresh: true) }) // MARK: - Error Alert diff --git a/Dashboard/Dashboard/Presentation/DashboardViewModel.swift b/Dashboard/Dashboard/Presentation/DashboardViewModel.swift index 1b5711508..0ffe9a547 100644 --- a/Dashboard/Dashboard/Presentation/DashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/DashboardViewModel.swift @@ -14,7 +14,7 @@ public class DashboardViewModel: ObservableObject { public var nextPage = 1 public var totalPages = 1 - public private(set) var fetchInProgress = false + @Published public private(set) var fetchInProgress = false @Published var courses: [CourseItem] = [] @Published var showError: Bool = false @@ -46,8 +46,9 @@ public class DashboardViewModel: ObservableObject { } @MainActor - public func getMyCourses(page: Int, withProgress: Bool = true, refresh: Bool = false) async { + public func getMyCourses(page: Int, refresh: Bool = false) async { do { + fetchInProgress = true if connectivity.isInternetAvaliable { if refresh { courses = try await interactor.getMyCourses(page: page) @@ -77,13 +78,13 @@ public class DashboardViewModel: ObservableObject { } @MainActor - public func getMyCoursesPagination(index: Int, withProgress: Bool = true) async { + public func getMyCoursesPagination(index: Int) async { if !fetchInProgress { if totalPages > 1 { if index == courses.count - 3 { if totalPages != 1 { if nextPage <= totalPages { - await getMyCourses(page: self.nextPage, withProgress: withProgress) + await getMyCourses(page: self.nextPage) } } } From 10f3a3fe84d89189c350c40aa5eedbd1cd87c85d Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Sat, 10 Jun 2023 15:40:10 +0300 Subject: [PATCH 17/26] fix discussion pagination issue (#37) --- .../Presentation/Comments/Thread/ThreadViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index 0457c8be0..aecb3202e 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -130,14 +130,14 @@ public class ThreadViewModel: BaseResponsesViewModel, ObservableObject { case .question: let (comments, pagination) = try await interactor .getQuestionComments(threadID: thread.id, page: page) - self.totalPages = totalPages + self.totalPages = pagination.numPages self.itemsCount = pagination.count self.comments += comments postComments = generateComments(comments: self.comments, thread: thread) case .discussion: let (comments, pagination) = try await interactor .getDiscussionComments(threadID: thread.id, page: page) - self.totalPages = totalPages + self.totalPages = pagination.numPages self.itemsCount = pagination.count self.comments += comments postComments = generateComments(comments: self.comments, thread: thread) From 5a86565522cfd95b15df5b707eab72600818a63a Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Thu, 15 Jun 2023 17:19:26 +0300 Subject: [PATCH 18/26] Update pods to actual versions (#39) --- Podfile | 12 ++++++------ Podfile.lock | 34 +++++++++++++++++----------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Podfile b/Podfile index 3777caffe..9166c04f7 100644 --- a/Podfile +++ b/Podfile @@ -4,9 +4,9 @@ use_frameworks! :linkage => :static abstract_target "App" do #Code style - pod 'SwiftLint', '0.49.1' + pod 'SwiftLint', '~> 0.5' #CodeGen for resources - pod 'SwiftGen', '~> 6.0' + pod 'SwiftGen', '~> 6.6' target "NewEdX" do inherit! :complete @@ -17,13 +17,13 @@ abstract_target "App" do project './Core/Core.xcodeproj' workspace './Core/Core.xcodeproj' #Networking - pod 'Alamofire', '5.6.4' + pod 'Alamofire', '~> 5.7' #Keychain pod 'KeychainSwift', '~> 20.0' #SwiftUI backward UIKit access - pod 'Introspect', '0.1.4' - pod 'Kingfisher', '~> 7.6.2' - pod 'Swinject', '2.8.2' + pod 'Introspect', '~> 0.6' + pod 'Kingfisher', '~> 7.8' + pod 'Swinject', '2.8.3' end target "Authorization" do diff --git a/Podfile.lock b/Podfile.lock index 20ab398af..bcdbf64d2 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,26 +1,26 @@ PODS: - - Alamofire (5.6.4) - - Introspect (0.1.4) + - Alamofire (5.7.1) + - Introspect (0.6.1) - KeychainSwift (20.0.0) - - Kingfisher (7.6.2) + - Kingfisher (7.8.0) - Sourcery (1.8.0): - Sourcery/CLI-Only (= 1.8.0) - Sourcery/CLI-Only (1.8.0) - SwiftGen (6.6.2) - - SwiftLint (0.49.1) + - SwiftLint (0.52.2) - SwiftyMocky (4.2.0): - Sourcery (= 1.8.0) - - Swinject (2.8.2) + - Swinject (2.8.3) DEPENDENCIES: - - Alamofire (= 5.6.4) - - Introspect (= 0.1.4) + - Alamofire (~> 5.7) + - Introspect (~> 0.6) - KeychainSwift (~> 20.0) - - Kingfisher (~> 7.6.2) - - SwiftGen (~> 6.0) - - SwiftLint (= 0.49.1) + - Kingfisher (~> 7.8) + - SwiftGen (~> 6.6) + - SwiftLint (~> 0.5) - SwiftyMocky (from `https://github.com/MakeAWishFoundation/SwiftyMocky.git`, tag `4.2.0`) - - Swinject (= 2.8.2) + - Swinject (= 2.8.3) SPEC REPOS: trunk: @@ -44,16 +44,16 @@ CHECKOUT OPTIONS: :tag: 4.2.0 SPEC CHECKSUMS: - Alamofire: 4e95d97098eacb88856099c4fc79b526a299e48c - Introspect: b62c4dd2063072327c21d618ef2bedc3c87bc366 + Alamofire: 0123a34370cb170936ae79a8df46cc62b2edeb88 + Introspect: 1b66ef0782311ff003007732e2d940c69545a7be KeychainSwift: 0ce6a4d13f7228054d1a71bb1b500448fb2ab837 - Kingfisher: 6c5449c6450c5239166510ba04afe374a98afc4f + Kingfisher: 0656e1b064bfc1ca1cd04e033f617a86559696e9 Sourcery: 6f5fe49b82b7e02e8c65560cbd52e1be67a1af2e SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c - SwiftLint: 32ee33ded0636d0905ef6911b2b67bbaeeedafa5 + SwiftLint: 1ac76dac888ca05cb0cf24d0c85887ec1209961d SwiftyMocky: c5e96e4ff76ec6dbf5a5941aeb039b5a546954a0 - Swinject: b4a11b31992e8668308dc594ba1cb9b3164a37ab + Swinject: 893c9a543000ac2f10ee4cbaf0933c6992c935d5 -PODFILE CHECKSUM: 581ff8dcee79309f445fdcb8360e69b153117532 +PODFILE CHECKSUM: 20ef878e00f4ddcb660df8166a4c969e80e80901 COCOAPODS: 1.12.0 From d336e8ac067e1644c941fd0ec99f0dc6c99a2be5 Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:38:20 +0300 Subject: [PATCH 19/26] Feature/new blocks navigation (#40) * New CourseUnit View and navigation --------- Co-authored-by: Volodymyr Chekyrta --- .swiftlint.yml | 3 +- .../Presentation/Base/FieldsView.swift | 145 ++- .../Presentation/Login/SignInView.swift | 138 +-- .../Registration/SignUpViewModel.swift | 9 +- .../ResetPasswordViewModel.swift | 1 - .../AuthorizationMock.generated.swift | 48 +- Core/Core.xcodeproj/project.pbxproj | 20 +- .../discussionIcon.imageset/Contents.json | 12 + .../discussionIcon.svg | 10 + Core/Core/Configuration/BaseRouter.swift | 8 +- Core/Core/Configuration/CSSInjector.swift | 24 +- Core/Core/Data/AppStorage.swift | 4 +- Core/Core/Data/Model/Data_Dashboard.swift | 8 +- .../Core/Data/Repository/AuthRepository.swift | 8 +- Core/Core/Extensions/CGColorExtension.swift | 10 +- Core/Core/Extensions/Sequence.swift | 15 - Core/Core/Extensions/StringExtension.swift | 23 +- Core/Core/Extensions/UIApplication+.swift | 13 - .../Extensions/UIApplicationExtension.swift | 36 + Core/Core/Extensions/ViewExtension.swift | 41 +- Core/Core/Network/API.swift | 7 + Core/Core/Network/DownloadManager.swift | 8 +- .../Core/Network/HeadersRedirectHandler.swift | 21 +- Core/Core/Network/RequestInterceptor.swift | 8 +- Core/Core/SwiftGen/Assets.swift | 1 + Core/Core/SwiftGen/Strings.swift | 40 + Core/Core/Theme.swift | 1 + Core/Core/View/Base/AlertView.swift | 80 +- Core/Core/View/Base/CourseButton.swift | 7 +- Core/Core/View/Base/CourseCellView.swift | 2 +- Core/Core/View/Base/HTMLFormattedText.swift | 13 +- Core/Core/View/Base/PickerMenu.swift | 12 +- Core/Core/View/Base/StyledButton.swift | 2 +- Core/Core/View/Base/TextWithUrls.swift | 4 +- Core/Core/View/Base/UnitButtonView.swift | 209 ++++ Core/Core/View/Base/WebBrowser.swift | 19 +- Core/Core/View/Base/WebUnitView.swift | 102 +- Core/Core/View/Base/WebUnitViewModel.swift | 2 + Core/Core/View/Base/WebView.swift | 83 +- Core/Core/en.lproj/Localizable.strings | 21 + Core/Core/uk.lproj/Localizable.strings | 21 + Course/Course.xcodeproj/project.pbxproj | 72 +- Course/Course/Data/CourseRepository.swift | 1100 ++++++++++------- .../Data/Network/CourseDetailsEndpoint.swift | 10 +- .../Data/Persistence/CoursePersistence.swift | 21 +- Course/Course/Domain/CourseInteractor.swift | 6 +- .../Container/CourseContainerView.swift | 6 + .../Container/CourseContainerViewModel.swift | 86 +- Course/Course/Presentation/CourseRouter.swift | 128 +- .../Details/CourseDetailsViewModel.swift | 18 +- .../Handouts/HandoutsUpdatesDetailView.swift | 69 +- .../Presentation/Handouts/HandoutsView.swift | 23 +- .../Handouts/HandoutsViewModel.swift | 18 +- .../Outline/ContinueWithView.swift | 137 ++ .../Outline/CourseBlocksView.swift | 217 ---- .../Outline/CourseBlocksViewModel.swift | 90 -- .../Outline/CourseOutlineView.swift | 110 +- .../Outline/CourseVerticalView.swift | 212 ++-- .../Outline/CourseVerticalViewModel.swift | 25 +- .../Unit/CourseNavigationView.swift | 157 ++- .../Presentation/Unit/CourseUnitView.swift | 446 ++++--- .../Unit/CourseUnitViewModel.swift | 94 +- .../Unit/Subviews/DiscussionView.swift | 37 + .../Unit/Subviews/EncodedVideoView.swift | 41 + .../Unit/Subviews/LessonProgressView.swift | 42 + .../Unit/Subviews/UnknownView.swift | 38 + .../Presentation/Unit/Subviews/WebView.swift | 23 + .../Unit/Subviews/YouTubeView.swift | 43 + .../Presentation/Unit/UnitButtonView.swift | 159 --- .../Video/EncodedVideoPlayer.swift | 85 +- .../Video/EncodedVideoPlayerViewModel.swift | 49 + .../Video/PlayerViewController.swift | 46 +- .../Presentation/Video/SubtittlesView.swift | 73 +- .../Video/VideoPlayerViewModel.swift | 34 +- .../Video/YouTubeVideoPlayer.swift | 189 +-- .../Video/YouTubeVideoPlayerViewModel.swift | 124 ++ Course/Course/SwiftGen/Strings.swift | 12 +- Course/Course/en.lproj/Localizable.strings | 6 +- Course/Course/uk.lproj/Localizable.strings | 4 +- Course/CourseTests/CourseMock.generated.swift | 65 +- .../CourseContainerViewModelTests.swift | 10 + .../Details/CourseDetailsViewModelTests.swift | 60 +- .../Unit/CourseUnitViewModelTests.swift | 215 ++-- .../Unit/VideoPlayerViewModelTests.swift | 46 +- Dashboard/Dashboard.xcodeproj/project.pbxproj | 16 +- .../Presentation/DashboardView.swift | 1 - .../DashboardMock.generated.swift | 24 +- Discovery/Discovery.xcodeproj/project.pbxproj | 16 +- .../Presentation/DiscoveryView.swift | 26 +- .../Discovery/Presentation/SearchView.swift | 5 +- .../DiscoveryMock.generated.swift | 24 +- .../Discussion.xcodeproj/project.pbxproj | 16 +- .../Data/Model/Data_CommentsResponse.swift | 27 +- .../Data/Model/Data_CreatedComment.swift | 4 +- .../Data/Network/DiscussionRepository.swift | 4 +- .../Comments/Responses/ResponsesView.swift | 262 ++-- .../Responses/ResponsesViewModel.swift | 6 +- .../Comments/Thread/ThreadView.swift | 14 +- .../Comments/Thread/ThreadViewModel.swift | 72 +- .../CreateNewThread/CreateNewThreadView.swift | 278 +++-- .../CreateNewThreadViewModel.swift | 8 +- .../Presentation/DiscussionRouter.swift | 10 +- .../DiscussionSearchTopicsViewModel.swift | 2 +- .../DiscussionTopicsViewModel.swift | 72 +- .../Presentation/Posts/PostsView.swift | 131 +- .../Presentation/Posts/PostsViewModel.swift | 35 +- Discussion/Discussion/SwiftGen/Strings.swift | 8 + .../Discussion/en.lproj/Localizable.strings | 3 + .../Discussion/uk.lproj/Localizable.strings | 3 + .../DiscussionMock.generated.swift | 119 +- .../Base/BaseResponsesViewModelTests.swift | 9 +- .../Comment/ThreadViewModelTests.swift | 36 +- ...DiscussionSearchTopicsViewModelTests.swift | 51 +- .../Posts/PostViewModelTests.swift | 32 +- .../Responses/ResponsesViewModelTests.swift | 18 +- NewEdX/AppDelegate.swift | 14 + NewEdX/CoreDataHandler.swift | 8 +- NewEdX/DI/AppAssembly.swift | 4 +- NewEdX/DI/ScreenAssembly.swift | 87 +- NewEdX/Router.swift | 241 ++-- Podfile | 1 - Podfile.lock | 1 + .../DeleteAccount/DeleteAccountView.swift | 15 +- .../EditProfile/EditProfileView.swift | 48 +- .../EditProfile/EditProfileViewModel.swift | 73 +- .../EditProfile/ProfileBottomSheet.swift | 8 +- .../Presentation/Profile/ProfileView.swift | 32 +- .../Profile/ProfileViewModel.swift | 6 +- .../Profile/Presentation/ProfileRouter.swift | 14 +- .../Presentation/Settings/SettingsView.swift | 21 +- .../Settings/SettingsViewModel.swift | 2 +- .../Settings/VideoQualityView.swift | 22 +- .../EditProfileViewModelTests.swift | 2 +- .../ProfileTests/ProfileMock.generated.swift | 48 +- 134 files changed, 4486 insertions(+), 3178 deletions(-) create mode 100644 Core/Core/Assets.xcassets/Discussions/discussionIcon.imageset/Contents.json create mode 100644 Core/Core/Assets.xcassets/Discussions/discussionIcon.imageset/discussionIcon.svg delete mode 100644 Core/Core/Extensions/Sequence.swift delete mode 100644 Core/Core/Extensions/UIApplication+.swift create mode 100644 Core/Core/Extensions/UIApplicationExtension.swift create mode 100644 Core/Core/View/Base/UnitButtonView.swift create mode 100644 Course/Course/Presentation/Outline/ContinueWithView.swift delete mode 100644 Course/Course/Presentation/Outline/CourseBlocksView.swift delete mode 100644 Course/Course/Presentation/Outline/CourseBlocksViewModel.swift create mode 100644 Course/Course/Presentation/Unit/Subviews/DiscussionView.swift create mode 100644 Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift create mode 100644 Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift create mode 100644 Course/Course/Presentation/Unit/Subviews/UnknownView.swift create mode 100644 Course/Course/Presentation/Unit/Subviews/WebView.swift create mode 100644 Course/Course/Presentation/Unit/Subviews/YouTubeView.swift delete mode 100644 Course/Course/Presentation/Unit/UnitButtonView.swift create mode 100644 Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift create mode 100644 Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 207446104..a92444936 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -36,7 +36,8 @@ excluded: # paths to ignore during linting. Takes precedence over `included`. force_try: error -line_length: 125 +line_length: 120 +type_body_length: 300 trailing_whitespace: ignores_empty_lines: true diff --git a/Authorization/Authorization/Presentation/Base/FieldsView.swift b/Authorization/Authorization/Presentation/Base/FieldsView.swift index f7b5fe513..c021b565c 100644 --- a/Authorization/Authorization/Presentation/Base/FieldsView.swift +++ b/Authorization/Authorization/Presentation/Base/FieldsView.swift @@ -19,49 +19,55 @@ struct FieldsView: View { @State private var text: String = "" var body: some View { - ForEach(0.. Void, okTapped: @escaping () -> Void) { - addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) - let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) as? (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void - perform?(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`) + open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void + perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`) } open func presentView(transitionStyle: UIModalTransitionStyle, view: any View) { @@ -544,7 +544,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { case m_showRegisterScreen case m_showForgotPasswordScreen case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) - case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) case m_presentView__transitionStyle_transitionStylecontent_content(Parameter, Parameter<() -> any View>) @@ -590,14 +590,16 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) return Matcher.ComparisonResult(results) - case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped)): + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsNextsectionname, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped, let lhsNextsectiontapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsNextsectionname, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped, let rhsNextsectiontapped)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectionname, rhs: rhsNextsectionname, with: matcher), lhsNextsectionname, rhsNextsectionname, "nextSectionName")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsImage, rhs: rhsImage, with: matcher), lhsImage, rhsImage, "image")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectiontapped, rhs: rhsNextsectiontapped, with: matcher), lhsNextsectiontapped, rhsNextsectiontapped, "nextSectionTapped")) return Matcher.ComparisonResult(results) case (.m_presentView__transitionStyle_transitionStyleview_view(let lhsTransitionstyle, let lhsView), .m_presentView__transitionStyle_transitionStyleview_view(let rhsTransitionstyle, let rhsView)): @@ -627,7 +629,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { case .m_showRegisterScreen: return 0 case .m_showForgotPasswordScreen: return 0 case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue - case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue case let .m_presentView__transitionStyle_transitionStylecontent_content(p0, p1): return p0.intValue + p1.intValue } @@ -644,7 +646,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { case .m_showRegisterScreen: return ".showRegisterScreen()" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" - case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped: return ".presentAlert(alertTitle:alertMessage:action:image:onCloseTapped:okTapped:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" case .m_presentView__transitionStyle_transitionStylecontent_content: return ".presentView(transitionStyle:content:)" } @@ -675,7 +677,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} public static func presentView(transitionStyle: Parameter, content: Parameter<() -> any View>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStylecontent_content(`transitionStyle`, `content`))} } @@ -714,8 +716,8 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, perform: @escaping (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { - return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`), performs: perform) + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>, perform: @escaping (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`), performs: perform) } public static func presentView(transitionStyle: Parameter, view: Parameter, perform: @escaping (UIModalTransitionStyle, any View) -> Void) -> Perform { return Perform(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`), performs: perform) @@ -902,10 +904,10 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`) } - open func presentAlert(alertTitle: String, alertMessage: String, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void) { - addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) - let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) as? (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void - perform?(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`) + open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void + perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`) } open func presentView(transitionStyle: UIModalTransitionStyle, view: any View) { @@ -932,7 +934,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showRegisterScreen case m_showForgotPasswordScreen case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) - case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) case m_presentView__transitionStyle_transitionStylecontent_content(Parameter, Parameter<() -> any View>) @@ -978,14 +980,16 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) return Matcher.ComparisonResult(results) - case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped)): + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsNextsectionname, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped, let lhsNextsectiontapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsNextsectionname, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped, let rhsNextsectiontapped)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectionname, rhs: rhsNextsectionname, with: matcher), lhsNextsectionname, rhsNextsectionname, "nextSectionName")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsImage, rhs: rhsImage, with: matcher), lhsImage, rhsImage, "image")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectiontapped, rhs: rhsNextsectiontapped, with: matcher), lhsNextsectiontapped, rhsNextsectiontapped, "nextSectionTapped")) return Matcher.ComparisonResult(results) case (.m_presentView__transitionStyle_transitionStyleview_view(let lhsTransitionstyle, let lhsView), .m_presentView__transitionStyle_transitionStyleview_view(let rhsTransitionstyle, let rhsView)): @@ -1015,7 +1019,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return 0 case .m_showForgotPasswordScreen: return 0 case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue - case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue case let .m_presentView__transitionStyle_transitionStylecontent_content(p0, p1): return p0.intValue + p1.intValue } @@ -1032,7 +1036,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return ".showRegisterScreen()" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" - case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped: return ".presentAlert(alertTitle:alertMessage:action:image:onCloseTapped:okTapped:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" case .m_presentView__transitionStyle_transitionStylecontent_content: return ".presentView(transitionStyle:content:)" } @@ -1063,7 +1067,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} public static func presentView(transitionStyle: Parameter, content: Parameter<() -> any View>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStylecontent_content(`transitionStyle`, `content`))} } @@ -1102,8 +1106,8 @@ open class BaseRouterMock: BaseRouter, Mock { public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, perform: @escaping (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { - return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`), performs: perform) + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>, perform: @escaping (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`), performs: perform) } public static func presentView(transitionStyle: Parameter, view: Parameter, perform: @escaping (UIModalTransitionStyle, any View) -> Void) -> Perform { return Perform(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`), performs: perform) diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 731ba83cb..8a681437e 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 021D925028DC89D100ACC565 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924F28DC89D100ACC565 /* UserProfile.swift */; }; 021D925728DCF12900ACC565 /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D925628DCF12900ACC565 /* AlertView.swift */; }; 02280F5B294B4E6F0032823A /* Connectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5A294B4E6F0032823A /* Connectivity.swift */; }; + 02284C182A3B1AE00007117F /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */; }; 022C64E429AE0191000F532B /* TextWithUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64E329AE0191000F532B /* TextWithUrls.swift */; }; 0231CDBE2922422D00032416 /* CSSInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0231CDBD2922422D00032416 /* CSSInjector.swift */; }; 0236961928F9A26900EEF206 /* AuthRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0236961828F9A26900EEF206 /* AuthRepository.swift */; }; @@ -34,6 +35,7 @@ 0251ED0C299D16BD00E70450 /* RefreshableScrollViewCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0251ED0B299D16BC00E70450 /* RefreshableScrollViewCompat.swift */; }; 0255D5582936283A004DBC1A /* UploadBodyEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0255D55729362839004DBC1A /* UploadBodyEncoding.swift */; }; 0259104A29C4A5B6004B5A55 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0259104929C4A5B6004B5A55 /* UserSettings.swift */; }; + 025B36752A13B7D5001A640E /* UnitButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025B36742A13B7D5001A640E /* UnitButtonView.swift */; }; 025EF2F62971740000B838AB /* YouTubePlayerKit in Frameworks */ = {isa = PBXBuildFile; productRef = 025EF2F52971740000B838AB /* YouTubePlayerKit */; }; 0260E58028FD792800BBBE18 /* WebUnitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0260E57F28FD792800BBBE18 /* WebUnitViewModel.swift */; }; 027BD3922907D88F00392132 /* Data_RegistrationFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3912907D88F00392132 /* Data_RegistrationFields.swift */; }; @@ -50,7 +52,6 @@ 027BD3B92909476200392132 /* KeyboardAvoidingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3B72909476200392132 /* KeyboardAvoidingModifier.swift */; }; 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3BA2909478B00392132 /* UIView+EnclosingScrollView.swift */; }; 027BD3BE2909478B00392132 /* UIResponder+CurrentResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3BB2909478B00392132 /* UIResponder+CurrentResponder.swift */; }; - 027BD3BF2909478B00392132 /* UIApplication+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3BC2909478B00392132 /* UIApplication+.swift */; }; 027BD3C52909707700392132 /* Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3C42909707700392132 /* Shake.swift */; }; 027DB33528D8C8FE002B6862 /* Data_MyCourse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33428D8C8FE002B6862 /* Data_MyCourse.swift */; }; 0282DA7328F98CC9003C3F07 /* WebUnitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0282DA7228F98CC9003C3F07 /* WebUnitView.swift */; }; @@ -62,7 +63,6 @@ 028F9F39293A452B00DE65D0 /* ResetPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028F9F38293A452B00DE65D0 /* ResetPassword.swift */; }; 0295B1DC297FF114003B0C65 /* SF-Pro.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0295B1DA297FF0E9003B0C65 /* SF-Pro.ttf */; }; 0295C885299B99DD00ABE571 /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */; }; - 029B78F1292517860097ACD8 /* Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029B78F0292517860097ACD8 /* Sequence.swift */; }; 02A4833529B8A73400D33F33 /* CorePersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833429B8A73400D33F33 /* CorePersistence.swift */; }; 02A4833829B8A8F900D33F33 /* CoreDataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833629B8A8F800D33F33 /* CoreDataModel.xcdatamodeld */; }; 02A4833A29B8A9AB00D33F33 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833929B8A9AB00D33F33 /* DownloadManager.swift */; }; @@ -134,6 +134,7 @@ 021D924F28DC89D100ACC565 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 021D925628DCF12900ACC565 /* AlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertView.swift; sourceTree = ""; }; 02280F5A294B4E6F0032823A /* Connectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.swift; sourceTree = ""; }; + 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; 022C64E329AE0191000F532B /* TextWithUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithUrls.swift; sourceTree = ""; }; 0231CDBD2922422D00032416 /* CSSInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSSInjector.swift; sourceTree = ""; }; 0236961828F9A26900EEF206 /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRepository.swift; sourceTree = ""; }; @@ -154,6 +155,7 @@ 0251ED0B299D16BC00E70450 /* RefreshableScrollViewCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollViewCompat.swift; sourceTree = ""; }; 0255D55729362839004DBC1A /* UploadBodyEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadBodyEncoding.swift; sourceTree = ""; }; 0259104929C4A5B6004B5A55 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; + 025B36742A13B7D5001A640E /* UnitButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitButtonView.swift; sourceTree = ""; }; 0260E57F28FD792800BBBE18 /* WebUnitViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebUnitViewModel.swift; sourceTree = ""; }; 027BD3912907D88F00392132 /* Data_RegistrationFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_RegistrationFields.swift; sourceTree = ""; }; 027BD39B2908810C00392132 /* RegisterUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterUser.swift; sourceTree = ""; }; @@ -169,7 +171,6 @@ 027BD3B72909476200392132 /* KeyboardAvoidingModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardAvoidingModifier.swift; sourceTree = ""; }; 027BD3BA2909478B00392132 /* UIView+EnclosingScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+EnclosingScrollView.swift"; sourceTree = ""; }; 027BD3BB2909478B00392132 /* UIResponder+CurrentResponder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIResponder+CurrentResponder.swift"; sourceTree = ""; }; - 027BD3BC2909478B00392132 /* UIApplication+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+.swift"; sourceTree = ""; }; 027BD3C42909707700392132 /* Shake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shake.swift; sourceTree = ""; }; 027DB33428D8C8FE002B6862 /* Data_MyCourse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_MyCourse.swift; sourceTree = ""; }; 0282DA7228F98CC9003C3F07 /* WebUnitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebUnitView.swift; sourceTree = ""; }; @@ -181,7 +182,6 @@ 028F9F38293A452B00DE65D0 /* ResetPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPassword.swift; sourceTree = ""; }; 0295B1DA297FF0E9003B0C65 /* SF-Pro.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro.ttf"; sourceTree = ""; }; 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; - 029B78F0292517860097ACD8 /* Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = ""; }; 02A4833429B8A73400D33F33 /* CorePersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePersistence.swift; sourceTree = ""; }; 02A4833729B8A8F800D33F33 /* CoreDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreDataModel.xcdatamodel; sourceTree = ""; }; 02A4833929B8A9AB00D33F33 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; @@ -331,17 +331,16 @@ 0283347F28D4DCD200C828FC /* ViewExtension.swift */, 02F6EF4928D9F0A700835477 /* DateExtension.swift */, 02F98A7E28F81EE900DE94C0 /* Container+App.swift */, - 027BD3BC2909478B00392132 /* UIApplication+.swift */, 027BD3BB2909478B00392132 /* UIResponder+CurrentResponder.swift */, 027BD3BA2909478B00392132 /* UIView+EnclosingScrollView.swift */, 02E225AF291D29EB0067769A /* UrlExtension.swift */, - 029B78F0292517860097ACD8 /* Sequence.swift */, 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */, 07460FE0294B706200F70538 /* CollectionExtension.swift */, 07460FE2294B72D700F70538 /* Notification.swift */, 02B2B593295C5C7A00914876 /* Thread.swift */, 0727878228D31287002E9142 /* DispatchQueue+App.swift */, 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */, + 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */, ); path = Extensions; sourceTree = ""; @@ -515,6 +514,7 @@ 02D800CB29348F460099CF16 /* ImagePicker.swift */, 024D723429C8BB1A006D36ED /* NavigationBar.swift */, 071009C328D1C9D000344290 /* StyledButton.swift */, + 025B36742A13B7D5001A640E /* UnitButtonView.swift */, 0727877C28D25212002E9142 /* ProgressBar.swift */, 022C64E329AE0191000F532B /* TextWithUrls.swift */, 0727878028D25EFD002E9142 /* SnackBarView.swift */, @@ -757,6 +757,7 @@ 027BD3B92909476200392132 /* KeyboardAvoidingModifier.swift in Sources */, 0770DE2C28D092B3006D8A5D /* NetworkLogger.swift in Sources */, 0770DE2A28D0929E006D8A5D /* HTTPTask.swift in Sources */, + 02284C182A3B1AE00007117F /* UIApplicationExtension.swift in Sources */, 0255D5582936283A004DBC1A /* UploadBodyEncoding.swift in Sources */, 027BD3B32909475900392132 /* Publishers+KeyboardState.swift in Sources */, 0727877D28D25212002E9142 /* ProgressBar.swift in Sources */, @@ -766,7 +767,6 @@ 0282DA7328F98CC9003C3F07 /* WebUnitView.swift in Sources */, 0727878128D25EFD002E9142 /* SnackBarView.swift in Sources */, 021D924828DC860C00ACC565 /* Data_UserProfile.swift in Sources */, - 029B78F1292517860097ACD8 /* Sequence.swift in Sources */, 070019AC28F6FD0100D5FC78 /* CourseDetailBlock.swift in Sources */, 0727877028D23411002E9142 /* Config.swift in Sources */, CFC84952299F8B890055E497 /* Debounce.swift in Sources */, @@ -813,7 +813,6 @@ 0248C92329C075EF00DC8402 /* CourseBlockModel.swift in Sources */, 072787B628D37A0E002E9142 /* Validator.swift in Sources */, 0236961D28F9A2D200EEF206 /* Data_AuthResponse.swift in Sources */, - 027BD3BF2909478B00392132 /* UIApplication+.swift in Sources */, 02B2B594295C5C7A00914876 /* Thread.swift in Sources */, 027DB33528D8C8FE002B6862 /* Data_MyCourse.swift in Sources */, 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */, @@ -838,6 +837,7 @@ 0236961928F9A26900EEF206 /* AuthRepository.swift in Sources */, 023A1136291432B200D0D354 /* RegistrationTextField.swift in Sources */, 02C2DC0829B63D6200F4445D /* WebViewHTML.swift in Sources */, + 025B36752A13B7D5001A640E /* UnitButtonView.swift in Sources */, 028F9F39293A452B00DE65D0 /* ResetPassword.swift in Sources */, 027BD3B82909476200392132 /* DismissKeyboardTapViewModifier.swift in Sources */, 024BE3DF29B2615500BCDEE2 /* CGColorExtension.swift in Sources */, @@ -1851,8 +1851,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SvenTiigi/YouTubePlayerKit"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 1.3.0; + kind = upToNextMajorVersion; + minimumVersion = 1.5.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Core/Core/Assets.xcassets/Discussions/discussionIcon.imageset/Contents.json b/Core/Core/Assets.xcassets/Discussions/discussionIcon.imageset/Contents.json new file mode 100644 index 000000000..9a7912c28 --- /dev/null +++ b/Core/Core/Assets.xcassets/Discussions/discussionIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "discussionIcon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Discussions/discussionIcon.imageset/discussionIcon.svg b/Core/Core/Assets.xcassets/Discussions/discussionIcon.imageset/discussionIcon.svg new file mode 100644 index 000000000..48897c349 --- /dev/null +++ b/Core/Core/Assets.xcassets/Discussions/discussionIcon.imageset/discussionIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Core/Core/Configuration/BaseRouter.swift b/Core/Core/Configuration/BaseRouter.swift index 168e094b9..c6c54ca67 100644 --- a/Core/Core/Configuration/BaseRouter.swift +++ b/Core/Core/Configuration/BaseRouter.swift @@ -41,10 +41,12 @@ public protocol BaseRouter { func presentAlert( alertTitle: String, alertMessage: String, + nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, - okTapped: @escaping () -> Void + okTapped: @escaping () -> Void, + nextSectionTapped: @escaping () -> Void ) func presentView(transitionStyle: UIModalTransitionStyle, view: any View) @@ -99,10 +101,12 @@ open class BaseRouterMock: BaseRouter { public func presentAlert( alertTitle: String, alertMessage: String, + nextSectionName: String? = nil, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, - okTapped: @escaping () -> Void + okTapped: @escaping () -> Void, + nextSectionTapped: @escaping () -> Void ) {} public func presentView(transitionStyle: UIModalTransitionStyle, view: any View) {} diff --git a/Core/Core/Configuration/CSSInjector.swift b/Core/Core/Configuration/CSSInjector.swift index c1fcd96b8..2101920d1 100644 --- a/Core/Core/Configuration/CSSInjector.swift +++ b/Core/Core/Configuration/CSSInjector.swift @@ -71,20 +71,28 @@ public class CSSInjector { } } - public func injectCSS(colorScheme: ColorScheme, html: String, - type: CssType, fontSize: Int = 150, screenWidth: CGFloat) -> String { - let meadiaReplace = html.replacingOccurrences(of: "/media/", - with: baseURL.absoluteString + "/media/") - var replacedHTML = meadiaReplace.replacingOccurrences(of: "../..", - with: baseURL.absoluteString) - .replacingOccurrences(of: "src=\"/", with: "src=\"" + baseURL.absoluteString + "/") + public func injectCSS( + colorScheme: ColorScheme, + html: String, + type: CssType, + fontSize: Int = 150, + screenWidth: CGFloat + ) -> String { + let meadiaReplace = html.replacingOccurrences( + of: "/media/", + with: baseURL.absoluteString + "/media/" + ) + var replacedHTML = meadiaReplace.replacingOccurrences( + of: "../..", + with: baseURL.absoluteString + ).replacingOccurrences(of: "src=\"/", with: "src=\"" + baseURL.absoluteString + "/") .replacingOccurrences(of: "href=\"/", with: "href=\"" + baseURL.absoluteString + "/") .replacingOccurrences(of: "href='/honor'", with: "href='\(baseURL.absoluteString)/honor'") .replacingOccurrences(of: "href='/privacy'", with: "href='\(baseURL.absoluteString)/privacy'") if colorScheme == .dark { replacedHTML = replaceHexColorsInHTML(html: replacedHTML) } - + var maxWidth: String switch type { case .discovery: diff --git a/Core/Core/Data/AppStorage.swift b/Core/Core/Data/AppStorage.swift index 48f088070..ee8bccccb 100644 --- a/Core/Core/Data/AppStorage.swift +++ b/Core/Core/Data/AppStorage.swift @@ -129,9 +129,9 @@ public class AppStorage { private let KEY_ACCESS_TOKEN = "accessToken" private let KEY_REFRESH_TOKEN = "refreshToken" private let KEY_COOKIES_DATE = "cookiesDate" - private let KEY_USER_PROFILE = "UserProfile" + private let KEY_USER_PROFILE = "userProfile" private let KEY_USER = "refreshToken" - private let KEY_SETTINGS = "UserSettings" + private let KEY_SETTINGS = "userSettings" } // Mark - For testing and SwiftUI preview diff --git a/Core/Core/Data/Model/Data_Dashboard.swift b/Core/Core/Data/Model/Data_Dashboard.swift index 86874824d..4133670a9 100644 --- a/Core/Core/Data/Model/Data_Dashboard.swift +++ b/Core/Core/Data/Model/Data_Dashboard.swift @@ -85,8 +85,12 @@ public extension DataLayer.CourseEnrollments { isActive: true, courseStart: course.course.start != nil ? Date(iso8601: course.course.start!) : nil, courseEnd: course.course.end != nil ? Date(iso8601: course.course.end!) : nil, - enrollmentStart: course.course.enrollmentStart != nil ? Date(iso8601: course.course.enrollmentStart!) : nil, - enrollmentEnd: course.course.enrollmentEnd != nil ? Date(iso8601: course.course.enrollmentEnd!) : nil, + enrollmentStart: course.course.enrollmentStart != nil + ? Date(iso8601: course.course.enrollmentStart!) + : nil, + enrollmentEnd: course.course.enrollmentEnd != nil + ? Date(iso8601: course.course.enrollmentEnd!) + : nil, courseID: course.course.id, numPages: numPages, coursesCount: count diff --git a/Core/Core/Data/Repository/AuthRepository.swift b/Core/Core/Data/Repository/AuthRepository.swift index 0689bc2c5..e945c8ea4 100644 --- a/Core/Core/Data/Repository/AuthRepository.swift +++ b/Core/Core/Data/Repository/AuthRepository.swift @@ -30,7 +30,11 @@ public class AuthRepository: AuthRepositoryProtocol { public func login(username: String, password: String) async throws -> User { appStorage.cookiesDate = nil - let endPoint = AuthEndpoint.getAccessToken(username: username, password: password, clientId: config.oAuthClientId) + let endPoint = AuthEndpoint.getAccessToken( + username: username, + password: password, + clientId: config.oAuthClientId + ) let authResponse = try await api.requestData(endPoint).mapResponse(DataLayer.AuthResponse.self) guard let accessToken = authResponse.accessToken, let refreshToken = authResponse.refreshToken else { @@ -64,8 +68,8 @@ public class AuthRepository: AuthRepositoryProtocol { appStorage.cookiesDate = Date().dateToString(style: .iso8601) } } else { - appStorage.cookiesDate = Date().dateToString(style: .iso8601) _ = try await api.requestData(AuthEndpoint.getAuthCookies) + appStorage.cookiesDate = Date().dateToString(style: .iso8601) } } diff --git a/Core/Core/Extensions/CGColorExtension.swift b/Core/Core/Extensions/CGColorExtension.swift index ac10df279..3086aaf61 100644 --- a/Core/Core/Extensions/CGColorExtension.swift +++ b/Core/Core/Extensions/CGColorExtension.swift @@ -16,10 +16,12 @@ public extension CGColor { let red = components[0] let green = components[1] let blue = components[2] - let hexString = String(format: "#%02lX%02lX%02lX", - lroundf(Float(red * 255)), - lroundf(Float(green * 255)), - lroundf(Float(blue * 255))) + let hexString = String( + format: "#%02lX%02lX%02lX", + lroundf(Float(red * 255)), + lroundf(Float(green * 255)), + lroundf(Float(blue * 255)) + ) return hexString } } diff --git a/Core/Core/Extensions/Sequence.swift b/Core/Core/Extensions/Sequence.swift deleted file mode 100644 index 4eefef060..000000000 --- a/Core/Core/Extensions/Sequence.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Sequence.swift -// Core -// -// Created by  Stepanok Ivan on 16.11.2022. -// - -import Foundation - -public extension Sequence where Element: Hashable { - func uniqued() -> [Element] { - var set = Set() - return filter { set.insert($0).inserted } - } -} diff --git a/Core/Core/Extensions/StringExtension.swift b/Core/Core/Extensions/StringExtension.swift index d9e3b57f6..3a5e6c141 100644 --- a/Core/Core/Extensions/StringExtension.swift +++ b/Core/Core/Extensions/StringExtension.swift @@ -21,10 +21,12 @@ public extension String { guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { return self } - return detector.stringByReplacingMatches(in: self, - options: [], - range: NSRange(location: 0, length: self.utf16.count), - withTemplate: "") + return detector.stringByReplacingMatches( + in: self, + options: [], + range: NSRange(location: 0, length: self.utf16.count), + withTemplate: "" + ) .replacingOccurrences(of: "<[^>]+>", with: "", options: String.CompareOptions.regularExpression, range: nil) .replacingOccurrences(of: "

", with: "") .replacingOccurrences(of: "

", with: "") @@ -39,12 +41,15 @@ public extension String { var urls: [URL] = [] do { let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - detector.enumerateMatches(in: self, options: [], - range: NSRange(location: 0, length: self.count), using: { (result, _, _) in - if let match = result, let url = match.url { - urls.append(url) + detector.enumerateMatches( + in: self, options: [], + range: NSRange(location: 0, length: self.count), + using: { (result, _, _) in + if let match = result, let url = match.url { + urls.append(url) + } } - }) + ) } catch let error as NSError { print(error.localizedDescription) } diff --git a/Core/Core/Extensions/UIApplication+.swift b/Core/Core/Extensions/UIApplication+.swift deleted file mode 100644 index 94ead3d50..000000000 --- a/Core/Core/Extensions/UIApplication+.swift +++ /dev/null @@ -1,13 +0,0 @@ -// - -import UIKit - -extension UIApplication { - var keyWindow: UIWindow? { - UIApplication.shared.windows.first { $0.isKeyWindow } - } - - func endEditing(force: Bool = true) { - windows.forEach { $0.endEditing(force) } - } -} diff --git a/Core/Core/Extensions/UIApplicationExtension.swift b/Core/Core/Extensions/UIApplicationExtension.swift new file mode 100644 index 000000000..1c5dec36c --- /dev/null +++ b/Core/Core/Extensions/UIApplicationExtension.swift @@ -0,0 +1,36 @@ +// +// UIApplicationExtension.swift +// Core +// +// Created by  Stepanok Ivan on 15.06.2023. +// + +import UIKit + +extension UIApplication { + + public var keyWindow: UIWindow? { + UIApplication.shared.windows.first { $0.isKeyWindow } + } + + public func endEditing(force: Bool = true) { + windows.forEach { $0.endEditing(force) } + } + + public class func topViewController( + controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController + ) -> UIViewController? { + if let navigationController = controller as? UINavigationController { + return topViewController(controller: navigationController.visibleViewController) + } + if let tabController = controller as? UITabBarController { + if let selected = tabController.selectedViewController { + return topViewController(controller: selected) + } + } + if let presented = controller?.presentedViewController { + return topViewController(controller: presented) + } + return controller + } +} diff --git a/Core/Core/Extensions/ViewExtension.swift b/Core/Core/Extensions/ViewExtension.swift index e5b1256cf..cab274996 100644 --- a/Core/Core/Extensions/ViewExtension.swift +++ b/Core/Core/Extensions/ViewExtension.swift @@ -44,12 +44,14 @@ public extension View { )) } - func cardStyle(top: CGFloat? = 0, - bottom: CGFloat? = 0, - leftLineEnabled: Bool = false, - bgColor: Color = CoreAssets.background.swiftUIColor, - strokeColor: Color = CoreAssets.cardViewStroke.swiftUIColor, - textColor: Color = CoreAssets.textPrimary.swiftUIColor) -> some View { + func cardStyle( + top: CGFloat? = 0, + bottom: CGFloat? = 0, + leftLineEnabled: Bool = false, + bgColor: Color = CoreAssets.background.swiftUIColor, + strokeColor: Color = CoreAssets.cardViewStroke.swiftUIColor, + textColor: Color = CoreAssets.textPrimary.swiftUIColor + ) -> some View { return self .padding(.all, 20) .padding(.vertical, leftLineEnabled ? 0 : 6) @@ -81,10 +83,12 @@ public extension View { .padding(.bottom, bottom) } - func shadowCardStyle(top: CGFloat? = 0, - bottom: CGFloat? = 0, - bgColor: Color = CoreAssets.cardViewBackground.swiftUIColor, - textColor: Color = CoreAssets.textPrimary.swiftUIColor) -> some View { + func shadowCardStyle( + top: CGFloat? = 0, + bottom: CGFloat? = 0, + bgColor: Color = CoreAssets.cardViewBackground.swiftUIColor, + textColor: Color = CoreAssets.textPrimary.swiftUIColor + ) -> some View { return self .padding(.all, 16) .padding(.vertical, 6) @@ -103,8 +107,11 @@ public extension View { } - func titleSettings(top: CGFloat? = 10, bottom: CGFloat? = 20, - color: Color = CoreAssets.textPrimary.swiftUIColor) -> some View { + func titleSettings( + top: CGFloat? = 10, + bottom: CGFloat? = 20, + color: Color = CoreAssets.textPrimary.swiftUIColor + ) -> some View { return self .lineLimit(1) .truncationMode(.tail) @@ -123,10 +130,12 @@ public extension View { } } - func roundedBackground(_ color: Color = CoreAssets.background.swiftUIColor, - strokeColor: Color = CoreAssets.backgroundStroke.swiftUIColor, - ipadMaxHeight: CGFloat = .infinity, - maxIpadWidth: CGFloat = 420) -> some View { + func roundedBackground( + _ color: Color = CoreAssets.background.swiftUIColor, + strokeColor: Color = CoreAssets.backgroundStroke.swiftUIColor, + ipadMaxHeight: CGFloat = .infinity, + maxIpadWidth: CGFloat = 420 + ) -> some View { var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } return ZStack { RoundedCorners(tl: 24, tr: 24) diff --git a/Core/Core/Network/API.swift b/Core/Core/Network/API.swift index 19bd2edc0..d891c7af5 100644 --- a/Core/Core/Network/API.swift +++ b/Core/Core/Network/API.swift @@ -7,6 +7,7 @@ import Foundation import Alamofire +import WebKit public final class API { @@ -127,6 +128,12 @@ public final class API { let cookies = HTTPCookie.cookies(withResponseHeaderFields: fields, for: url) HTTPCookieStorage.shared.cookies?.forEach { HTTPCookieStorage.shared.deleteCookie($0) } HTTPCookieStorage.shared.setCookies(cookies, for: url, mainDocumentURL: nil) + DispatchQueue.main.async { + let cookies = HTTPCookieStorage.shared.cookies ?? [] + for c in cookies { + WKWebsiteDataStore.default().httpCookieStore.setCookie(c) + } + } } private func callResponse( diff --git a/Core/Core/Network/DownloadManager.swift b/Core/Core/Network/DownloadManager.swift index 0392c13f5..7166f5b90 100644 --- a/Core/Core/Network/DownloadManager.swift +++ b/Core/Core/Network/DownloadManager.swift @@ -188,12 +188,14 @@ public class DownloadManager: DownloadManagerProtocol { let directoryURL = documentDirectoryURL.appendingPathComponent("Files", isDirectory: true) if FileManager.default.fileExists(atPath: directoryURL.path) { - print(directoryURL.path) return URL(fileURLWithPath: directoryURL.path) } else { do { - try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) - print(directoryURL.path) + try FileManager.default.createDirectory( + at: directoryURL, + withIntermediateDirectories: true, + attributes: nil + ) return URL(fileURLWithPath: directoryURL.path) } catch { print(error.localizedDescription) diff --git a/Core/Core/Network/HeadersRedirectHandler.swift b/Core/Core/Network/HeadersRedirectHandler.swift index ccb60e29e..3653392bd 100644 --- a/Core/Core/Network/HeadersRedirectHandler.swift +++ b/Core/Core/Network/HeadersRedirectHandler.swift @@ -17,16 +17,17 @@ public class HeadersRedirectHandler: RedirectHandler { _ task: URLSessionTask, willBeRedirectedTo request: URLRequest, for response: HTTPURLResponse, - completion: @escaping (URLRequest?) -> Void) { - var redirectedRequest = request - - if let originalRequest = task.originalRequest, - let headers = originalRequest.allHTTPHeaderFields { - for (key, value) in headers { - redirectedRequest.setValue(value, forHTTPHeaderField: key) - } + completion: @escaping (URLRequest?) -> Void + ) { + var redirectedRequest = request + + if let originalRequest = task.originalRequest, + let headers = originalRequest.allHTTPHeaderFields { + for (key, value) in headers { + redirectedRequest.setValue(value, forHTTPHeaderField: key) } - - completion(redirectedRequest) } + + completion(redirectedRequest) + } } diff --git a/Core/Core/Network/RequestInterceptor.swift b/Core/Core/Network/RequestInterceptor.swift index 0915d4f84..3f8e80b9a 100644 --- a/Core/Core/Network/RequestInterceptor.swift +++ b/Core/Core/Network/RequestInterceptor.swift @@ -27,10 +27,10 @@ final public class RequestInterceptor: Alamofire.RequestInterceptor { _ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { - // guard urlRequest.url?.absoluteString.hasPrefix("https://api.authenticated.com") == true else { - // /// If the request does not require authentication, we can directly return it as unmodified. - // return completion(.success(urlRequest)) - // } +// guard urlRequest.url?.absoluteString.hasPrefix("https://api.authenticated.com") == true else { +// // If the request does not require authentication, we can directly return it as unmodified. +// return completion(.success(urlRequest)) +// } var urlRequest = urlRequest // Set the Authorization header value using the access token. diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index e9d8dfc49..887501306 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -57,6 +57,7 @@ public enum CoreAssets { public static let allPosts = ImageAsset(name: "allPosts") public static let chapter = ImageAsset(name: "chapter") public static let discussion = ImageAsset(name: "discussion") + public static let discussionIcon = ImageAsset(name: "discussionIcon") public static let extra = ImageAsset(name: "extra") public static let filter = ImageAsset(name: "filter") public static let finished = ImageAsset(name: "finished") diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index 6790017a0..0197b0494 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -24,6 +24,36 @@ public enum CoreLocalization { /// Log out public static let logout = CoreLocalization.tr("Localizable", "ALERT.LOGOUT", fallback: "Log out") } + public enum Courseware { + /// Back to outline + public static let backToOutline = CoreLocalization.tr("Localizable", "COURSEWARE.BACK_TO_OUTLINE", fallback: "Back to outline") + /// Continue + public static let `continue` = CoreLocalization.tr("Localizable", "COURSEWARE.CONTINUE", fallback: "Continue") + /// Continue with: + public static let continueWith = CoreLocalization.tr("Localizable", "COURSEWARE.CONTINUE_WITH", fallback: "Continue with:") + /// Course content + public static let courseContent = CoreLocalization.tr("Localizable", "COURSEWARE.COURSE_CONTENT", fallback: "Course content") + /// Course units + public static let courseUnits = CoreLocalization.tr("Localizable", "COURSEWARE.COURSE_UNITS", fallback: "Course units") + /// Finish + public static let finish = CoreLocalization.tr("Localizable", "COURSEWARE.FINISH", fallback: "Finish") + /// Good Work! + public static let goodWork = CoreLocalization.tr("Localizable", "COURSEWARE.GOOD_WORK", fallback: "Good Work!") + /// “ is finished. + public static let isFinished = CoreLocalization.tr("Localizable", "COURSEWARE.IS_FINISHED", fallback: "“ is finished.") + /// Next + public static let next = CoreLocalization.tr("Localizable", "COURSEWARE.NEXT", fallback: "Next") + /// Next section + public static let nextSection = CoreLocalization.tr("Localizable", "COURSEWARE.NEXT_SECTION", fallback: "Next section") + /// To proceed with “ + public static let nextSectionDescriptionFirst = CoreLocalization.tr("Localizable", "COURSEWARE.NEXT_SECTION_DESCRIPTION_FIRST", fallback: "To proceed with “") + /// ” press “Next section”. + public static let nextSectionDescriptionLast = CoreLocalization.tr("Localizable", "COURSEWARE.NEXT_SECTION_DESCRIPTION_LAST", fallback: "” press “Next section”.") + /// Prev + public static let previous = CoreLocalization.tr("Localizable", "COURSEWARE.PREVIOUS", fallback: "Prev") + /// Section “ + public static let section = CoreLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "Section “") + } public enum Date { /// Ended public static let ended = CoreLocalization.tr("Localizable", "DATE.ENDED", fallback: "Ended") @@ -53,6 +83,8 @@ public enum CoreLocalization { public static let invalidCredentials = CoreLocalization.tr("Localizable", "ERROR.INVALID_CREDENTIALS", fallback: "Invalid credentials") /// No cached data for offline mode public static let noCachedData = CoreLocalization.tr("Localizable", "ERROR.NO_CACHED_DATA", fallback: "No cached data for offline mode") + /// Reload + public static let reload = CoreLocalization.tr("Localizable", "ERROR.RELOAD", fallback: "Reload") /// Slow or no internet connection public static let slowOrNoInternetConnection = CoreLocalization.tr("Localizable", "ERROR.SLOW_OR_NO_INTERNET_CONNECTION", fallback: "Slow or no internet connection") /// Something went wrong @@ -97,6 +129,14 @@ public enum CoreLocalization { public static let tryAgainBtn = CoreLocalization.tr("Localizable", "VIEW.SNACKBAR.TRY_AGAIN_BTN", fallback: "Try Again") } } + public enum Webview { + public enum Alert { + /// Cancel + public static let cancel = CoreLocalization.tr("Localizable", "WEBVIEW.ALERT.CANCEL", fallback: "Cancel") + /// Ok + public static let ok = CoreLocalization.tr("Localizable", "WEBVIEW.ALERT.OK", fallback: "Ok") + } + } } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces diff --git a/Core/Core/Theme.swift b/Core/Core/Theme.swift index 57bc2bcd7..d8eb84573 100644 --- a/Core/Core/Theme.swift +++ b/Core/Core/Theme.swift @@ -38,6 +38,7 @@ public struct Theme { public static let cardImageRadius = 10.0 public static let textInputShape = RoundedRectangle(cornerRadius: 8) public static let buttonShape = RoundedCorners(tl: 8, tr: 8, bl: 8, br: 8) + public static let unitButtonShape = RoundedCorners(tl: 21, tr: 21, bl: 21, br: 21) public static let roundedScreenBackgroundShape = RoundedCorners( tl: Theme.Shapes.screenBackgroundRadius, tr: Theme.Shapes.screenBackgroundRadius, diff --git a/Core/Core/View/Base/AlertView.swift b/Core/Core/View/Base/AlertView.swift index 7a51d0bf8..d11ec70db 100644 --- a/Core/Core/View/Base/AlertView.swift +++ b/Core/Core/View/Base/AlertView.swift @@ -27,8 +27,10 @@ public struct AlertView: View { private var alertTitle: String private var alertMessage: String + private var nextSectionName: String? private var onCloseTapped: (() -> Void) = {} private var okTapped: (() -> Void) = {} + private var nextSectionTapped: (() -> Void) = {} private let type: AlertViewType public init( @@ -49,15 +51,19 @@ public struct AlertView: View { public init( alertTitle: String, alertMessage: String, + nextSectionName: String? = nil, mainAction: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, - okTapped: @escaping () -> Void + okTapped: @escaping () -> Void, + nextSectionTapped: @escaping () -> Void ) { self.alertTitle = alertTitle self.alertMessage = alertMessage self.onCloseTapped = onCloseTapped + self.nextSectionName = nextSectionName self.okTapped = okTapped + self.nextSectionTapped = nextSectionTapped type = .action(mainAction, image) } @@ -99,6 +105,7 @@ public struct AlertView: View { .font(Theme.Fonts.bodyMedium) .multilineTextAlignment(.center) .padding(.horizontal, 40) + .frame(maxWidth: 250) } HStack { switch type { @@ -109,8 +116,29 @@ public struct AlertView: View { .frame(maxWidth: 135) .saturation(0) case let .action(action, _): - StyledButton(action, action: { okTapped() }) - .frame(maxWidth: 160) + VStack(spacing: 20) { + if let nextSectionName { + UnitButtonView(type: .nextSection, action: { nextSectionTapped() }) + .frame(maxWidth: 215) + } + UnitButtonView(type: .custom(action), + bgColor: .clear, + action: { okTapped() }) + .frame(maxWidth: 215) + + if let nextSectionName { + Group { + Text(CoreLocalization.Courseware.nextSectionDescriptionFirst) + + Text(nextSectionName) + + Text(CoreLocalization.Courseware.nextSectionDescriptionLast) + }.frame(maxWidth: 215) + .padding(.horizontal, 40) + .multilineTextAlignment(.center) + .font(Theme.Fonts.labelSmall) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) + } + + } case .logOut: Button(action: { okTapped() @@ -133,7 +161,12 @@ public struct AlertView: View { ) .overlay( RoundedRectangle(cornerRadius: 8) - .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) + .stroke(style: .init( + lineWidth: 1, + lineCap: .round, + lineJoin: .round, + miterLimit: 1 + )) .foregroundColor(.clear) ) .frame(maxWidth: 215) @@ -157,7 +190,12 @@ public struct AlertView: View { ) .overlay( RoundedRectangle(cornerRadius: 8) - .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) + .stroke(style: .init( + lineWidth: 1, + lineCap: .round, + lineJoin: .round, + miterLimit: 1 + )) .foregroundColor(.clear) ) .frame(maxWidth: 215) @@ -180,7 +218,12 @@ public struct AlertView: View { ) .overlay( RoundedRectangle(cornerRadius: 8) - .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) + .stroke(style: .init( + lineWidth: 1, + lineCap: .round, + lineJoin: .round, + miterLimit: 1 + )) .foregroundColor(CoreAssets.textPrimary.swiftUIColor) ) .frame(maxWidth: 215) @@ -217,14 +260,23 @@ public struct AlertView: View { // swiftlint:disable all struct AlertView_Previews: PreviewProvider { static var previews: some View { - AlertView( - alertTitle: "Warning!", - alertMessage: "Something goes wrong. Do you want to exterminate your phone, right now", - positiveAction: "Accept", - onCloseTapped: {}, - okTapped: {}, - type: .logOut - ) +// AlertView( +// alertTitle: "Warning!", +// alertMessage: "Something goes wrong. Do you want to exterminate your phone, right now", +// positiveAction: "Accept", +// onCloseTapped: {}, +// okTapped: {}, +// type: .action("", CoreAssets.goodWork.swiftUIImage) +// ) + AlertView(alertTitle: "Warning", + alertMessage: "Something goes wrong. Do you want to exterminate your phone, right now", + nextSectionName: "Ahmad tea is a power", + mainAction: "Back to outline", + image: CoreAssets.goodWork.swiftUIImage, + onCloseTapped: {}, + okTapped: {}, + nextSectionTapped: {}) + .previewLayout(.sizeThatFits) .background(Color.gray) } diff --git a/Core/Core/View/Base/CourseButton.swift b/Core/Core/View/Base/CourseButton.swift index c3a607e35..a7473e36e 100644 --- a/Core/Core/View/Base/CourseButton.swift +++ b/Core/Core/View/Base/CourseButton.swift @@ -59,6 +59,11 @@ public struct CourseButton: View { struct CourseButton_Previews: PreviewProvider { static var previews: some View { - CourseButton(isCompleted: true, image: CoreAssets.pen.swiftUIImage, displayName: "Lets see whats happen", index: 0) + CourseButton( + isCompleted: true, + image: CoreAssets.pen.swiftUIImage, + displayName: "Lets see whats happen", + index: 0 + ) } } diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift index 1388d1489..dfb0b6925 100644 --- a/Core/Core/View/Base/CourseCellView.swift +++ b/Core/Core/View/Base/CourseCellView.swift @@ -33,7 +33,7 @@ public struct CourseCellView: View { self.courseStart = model.courseStart?.dateToString(style: .startDDMonthYear) ?? "" self.courseEnd = model.courseEnd?.dateToString(style: .endedMonthDay) ?? "" self.courseOrg = model.org - self.index = Double(index)+1 + self.index = Double(index) + 1 self.cellsCount = cellsCount } diff --git a/Core/Core/View/Base/HTMLFormattedText.swift b/Core/Core/View/Base/HTMLFormattedText.swift index d29f9e8b0..6276b9f2d 100644 --- a/Core/Core/View/Base/HTMLFormattedText.swift +++ b/Core/Core/View/Base/HTMLFormattedText.swift @@ -70,11 +70,14 @@ public struct HTMLFormattedText: UIViewRepresentable { private func convertHTML(text: String) -> NSAttributedString? { guard let data = text.data(using: .utf8) else { return nil } - if let attributedString = try? NSAttributedString(data: data, - options: [ - .documentType: NSAttributedString.DocumentType.html, - .characterEncoding: String.Encoding.utf8.rawValue - ], documentAttributes: nil) { + if let attributedString = try? NSAttributedString( + data: data, + options: [ + .documentType: NSAttributedString.DocumentType.html, + .characterEncoding: String.Encoding.utf8.rawValue + ], + documentAttributes: nil + ) { return attributedString } else { return nil diff --git a/Core/Core/View/Base/PickerMenu.swift b/Core/Core/View/Base/PickerMenu.swift index ab2377f89..ea0a78ce5 100644 --- a/Core/Core/View/Base/PickerMenu.swift +++ b/Core/Core/View/Base/PickerMenu.swift @@ -32,11 +32,13 @@ public struct PickerMenu: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } private var selected: ((PickerItem) -> Void) = { _ in } - public init(items: [PickerItem], - titleText: String, - router: BaseRouter, - selectedItem: PickerItem? = nil, - selected: @escaping (PickerItem) -> Void) { + public init( + items: [PickerItem], + titleText: String, + router: BaseRouter, + selectedItem: PickerItem? = nil, + selected: @escaping (PickerItem) -> Void + ) { self.items = items self.titleText = titleText self.router = router diff --git a/Core/Core/View/Base/StyledButton.swift b/Core/Core/View/Base/StyledButton.swift index 4e1a0b872..65d223447 100644 --- a/Core/Core/View/Base/StyledButton.swift +++ b/Core/Core/View/Base/StyledButton.swift @@ -43,7 +43,7 @@ public struct StyledButton: View { .frame(maxWidth: .infinity) .padding(.horizontal, 16) } - .frame(maxWidth: idiom == .pad ? 260: .infinity, minHeight: isTransparent ? 36 : 48) + .frame(maxWidth: idiom == .pad ? 260: .infinity, minHeight: isTransparent ? 36 : 42) .background( Theme.Shapes.buttonShape .fill(isTransparent ? .clear : buttonColor) diff --git a/Core/Core/View/Base/TextWithUrls.swift b/Core/Core/View/Base/TextWithUrls.swift index 70bc6934b..e5e50d19e 100644 --- a/Core/Core/View/Base/TextWithUrls.swift +++ b/Core/Core/View/Base/TextWithUrls.swift @@ -62,7 +62,9 @@ public struct TextWithUrls: View { var text = Text("") attributedString.enumerateAttributes(in: stringRange, options: []) { attrs, range, _ in let valueOfString: String = attributedString.attributedSubstring(from: range).string - text = text + Text(.init((attrs[.underlineStyle] != nil ? getMarkupText(url: valueOfString): valueOfString))) + text = text + Text(.init((attrs[.underlineStyle] != nil + ? getMarkupText(url: valueOfString) + : valueOfString))) } return text diff --git a/Core/Core/View/Base/UnitButtonView.swift b/Core/Core/View/Base/UnitButtonView.swift new file mode 100644 index 000000000..52b3900c9 --- /dev/null +++ b/Core/Core/View/Base/UnitButtonView.swift @@ -0,0 +1,209 @@ +// +// UnitButtonView.swift +// Course +// +// Created by  Stepanok Ivan on 14.02.2023. +// + +import SwiftUI + +public enum UnitButtonType: Equatable { + case first + case next + case nextBig + case previous + case last + case finish + case reload + case continueLesson + case nextSection + case custom(String) + + func stringValue() -> String { + switch self { + case .first: + return CoreLocalization.Courseware.next + case .next, .nextBig: + return CoreLocalization.Courseware.next + case .previous: + return CoreLocalization.Courseware.previous + case .last: + return CoreLocalization.Courseware.finish + case .finish: + return CoreLocalization.Courseware.finish + case .reload: + return CoreLocalization.Error.reload + case .continueLesson: + return CoreLocalization.Courseware.continue + case .nextSection: + return CoreLocalization.Courseware.nextSection + case let .custom(text): + return text + } + } +} + +public struct UnitButtonView: View { + + private let action: () -> Void + private let type: UnitButtonType + private let bgColor: Color? + + public init(type: UnitButtonType, bgColor: Color? = nil, action: @escaping () -> Void) { + self.type = type + self.bgColor = bgColor + self.action = action + } + + public var body: some View { + HStack { + Button(action: action) { + VStack { + switch type { + case .first: + HStack { + Text(type.stringValue()) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) + .font(Theme.Fonts.labelLarge) + CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) + .rotationEffect(Angle.degrees(-90)) + }.padding(.horizontal, 16) + case .next, .nextBig: + HStack { + Text(type.stringValue()) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) + .padding(.leading, 20) + .font(Theme.Fonts.labelLarge) + if type != .nextBig { + Spacer() + } + CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) + .rotationEffect(Angle.degrees(-90)) + .padding(.trailing, 20) + } + case .previous: + HStack { + Text(type.stringValue()) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) + .font(Theme.Fonts.labelLarge) + .padding(.leading, 20) + CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) + .rotationEffect(Angle.degrees(90)) + .padding(.trailing, 20) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) + + } + case .last: + HStack { + Text(type.stringValue()) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) + .padding(.leading, 16) + .font(Theme.Fonts.labelLarge) + Spacer() + CoreAssets.check.swiftUIImage.renderingMode(.template) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) + .padding(.trailing, 16) + } + case .finish: + HStack { + Text(type.stringValue()) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) + .font(Theme.Fonts.labelLarge) + CoreAssets.check.swiftUIImage.renderingMode(.template) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) + }.padding(.horizontal, 16) + case .reload, .custom: + VStack(alignment: .center) { + Text(type.stringValue()) + .foregroundColor(bgColor == nil ? .white : CoreAssets.accentColor.swiftUIColor) + .font(Theme.Fonts.labelLarge) + }.padding(.horizontal, 16) + case .continueLesson, .nextSection: + HStack { + Text(type.stringValue()) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) + .padding(.leading, 20) + .font(Theme.Fonts.labelLarge) + CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) + .rotationEffect(Angle.degrees(180)) + .padding(.trailing, 20) + } + } + } + .frame(maxWidth: .infinity, minHeight: 42) + .background( + VStack { + switch self.type { + case .first, .next, .nextBig, .previous, .last: + Theme.Shapes.buttonShape + .fill(type == .previous + ? CoreAssets.background.swiftUIColor + : CoreAssets.accentColor.swiftUIColor) + .shadow(color: Color.black.opacity(0.25), radius: 21, y: 4) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(style: .init( + lineWidth: 1, + lineCap: .round, + lineJoin: .round, + miterLimit: 1) + ) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) + ) + + case .continueLesson, .nextSection, .reload, .finish, .custom: + Theme.Shapes.buttonShape + .fill(bgColor ?? CoreAssets.accentColor.swiftUIColor) + + .shadow(color: (type == .first + || type == .next + || type == .previous + || type == .last + || type == .finish + || type == .reload) ? Color.black.opacity(0.25) : .clear, + radius: 21, y: 4) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(style: .init( + lineWidth: 1, + lineCap: .round, + lineJoin: .round, + miterLimit: 1 + )) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) + ) + } + } + ) + + } + .fixedSize(horizontal: (type == .first + || type == .next + || type == .previous + || type == .last + || type == .finish + || type == .reload) + , vertical: false) + } + + } +} + +struct UnitButtonView_Previews: PreviewProvider { + static var previews: some View { + VStack { + UnitButtonView(type: .first, action: {}) + UnitButtonView(type: .previous, action: {}) + UnitButtonView(type: .next, action: {}) + UnitButtonView(type: .last, action: {}) + UnitButtonView(type: .finish, action: {}) + UnitButtonView(type: .reload, action: {}) + UnitButtonView(type: .custom("Custom text"), action: {}) + UnitButtonView(type: .continueLesson, action: {}) + UnitButtonView(type: .nextSection, action: {}) + }.padding() + } +} diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift index 1919ce173..6d89e4528 100644 --- a/Core/Core/View/Base/WebBrowser.swift +++ b/Core/Core/View/Base/WebBrowser.swift @@ -6,6 +6,7 @@ // import SwiftUI +import WebKit public struct WebBrowser: View { @@ -24,17 +25,23 @@ public struct WebBrowser: View { // MARK: - Page name VStack(alignment: .center) { - NavigationBar(title: pageTitle, - leftButtonAction: { presentationMode.wrappedValue.dismiss() }) + NavigationBar( + title: pageTitle, + leftButtonAction: { presentationMode.wrappedValue.dismiss() } + ) // MARK: - Page Body VStack { ZStack(alignment: .top) { NavigationView { - WebView(viewModel: .init(url: url, baseURL: ""), isLoading: $isShowProgress, refreshCookies: {}) - .navigationBarTitle(Text("")) // Needed for hide navBar on ios 14, 15 - .navigationBarHidden(true) - .ignoresSafeArea() + WebView( + viewModel: .init(url: url, baseURL: ""), + isLoading: $isShowProgress, + refreshCookies: {} + ) + .navigationBarTitle(Text("")) // Needed for hide navBar on ios 14, 15 + .navigationBarHidden(true) + .ignoresSafeArea() } } } diff --git a/Core/Core/View/Base/WebUnitView.swift b/Core/Core/View/Base/WebUnitView.swift index b9dc4e772..85285a21b 100644 --- a/Core/Core/View/Base/WebUnitView.swift +++ b/Core/Core/View/Base/WebUnitView.swift @@ -17,66 +17,64 @@ public struct WebUnitView: View { public init(url: String, viewModel: WebUnitViewModel) { self.viewModel = viewModel self.url = url - Task { - await viewModel.updateCookies() - } } @ViewBuilder public var body: some View { - ZStack(alignment: .center) { - GeometryReader { reader in - ScrollView { - if viewModel.cookiesReady { - WebView( - viewModel: .init(url: url, baseURL: viewModel.config.baseURL.absoluteString), - isLoading: $isWebViewLoading, refreshCookies: { - await viewModel.updateCookies(force: true) + // MARK: - Error Alert + if viewModel.showError { + VStack(spacing: 28) { + Image(systemName: "nosign") + .resizable() + .scaledToFit() + .frame(width: 64) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) + Text(viewModel.errorMessage ?? "") + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) + Button(action: { + Task { + await viewModel.updateCookies(force: true) + } + }, label: { + Text(CoreLocalization.View.Snackbar.tryAgainBtn) + .frame(maxWidth: .infinity, minHeight: 48) + .background(Theme.Shapes.buttonShape.fill(.clear)) + .overlay(RoundedRectangle(cornerRadius: 8) + .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) + ) + }) + .frame(width: 100) + }.frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + ZStack(alignment: .center) { + GeometryReader { reader in + ScrollView { + if viewModel.cookiesReady { + WebView( + viewModel: .init(url: url, baseURL: viewModel.config.baseURL.absoluteString), + isLoading: $isWebViewLoading, refreshCookies: { + await viewModel.updateCookies(force: true) + }) + .introspectScrollView(customize: { scrollView in + scrollView.isScrollEnabled = false }) - .introspectScrollView(customize: { scrollView in - scrollView.isScrollEnabled = false - scrollView.alwaysBounceVertical = false - scrollView.alwaysBounceHorizontal = false - scrollView.bounces = false - }) - .frame(width: reader.size.width, height: reader.size.height) + .frame(width: reader.size.width, height: reader.size.height) + } } - } - if viewModel.updatingCookies || isWebViewLoading { - VStack { - ProgressBar(size: 40, lineWidth: 8) + if viewModel.updatingCookies || isWebViewLoading { + VStack { + ProgressBar(size: 40, lineWidth: 8) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) } - .frame(maxWidth: .infinity, maxHeight: .infinity) } - } - - // MARK: - Error Alert - if viewModel.showError { - VStack(spacing: 28) { - Image(systemName: "nosign") - .resizable() - .scaledToFit() - .frame(width: 64) - .foregroundColor(.black) - Text(viewModel.errorMessage ?? "") - .foregroundColor(.black) - .multilineTextAlignment(.center) - .padding(.horizontal, 20) - Button(action: { - Task { - await viewModel.updateCookies(force: true) - } - }, label: { - Text(CoreLocalization.View.Snackbar.tryAgainBtn) - .frame(maxWidth: .infinity, minHeight: 48) - .background(Theme.Shapes.buttonShape.fill(.clear)) - .overlay(RoundedRectangle(cornerRadius: 8) - .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) - .foregroundColor(CoreAssets.accentColor.swiftUIColor) - ) - }) - .frame(width: 100) - }.frame(maxWidth: .infinity, maxHeight: .infinity) + }.onFirstAppear { + Task { + await viewModel.updateCookies() + } } } } diff --git a/Core/Core/View/Base/WebUnitViewModel.swift b/Core/Core/View/Base/WebUnitViewModel.swift index 866f5dba4..caa010a93 100644 --- a/Core/Core/View/Base/WebUnitViewModel.swift +++ b/Core/Core/View/Base/WebUnitViewModel.swift @@ -33,11 +33,13 @@ public class WebUnitViewModel: ObservableObject { @MainActor func updateCookies(force: Bool = false) async { + guard !updatingCookies else { return } do { updatingCookies = true try await authInteractor.getCookies(force: force) cookiesReady = true updatingCookies = false + errorMessage = nil } catch { if error.isInternetError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection diff --git a/Core/Core/View/Base/WebView.swift b/Core/Core/View/Base/WebView.swift index 4ad2f9d5c..463a9a315 100644 --- a/Core/Core/View/Base/WebView.swift +++ b/Core/Core/View/Base/WebView.swift @@ -32,7 +32,7 @@ public struct WebView: UIViewRepresentable { self.refreshCookies = refreshCookies } - public class Coordinator: NSObject, WKNavigationDelegate { + public class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate { var parent: WebView init(_ parent: WebView) { @@ -45,9 +45,36 @@ public struct WebView: UIViewRepresentable { } } - public func webView(_ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { + public func webView( + _ webView: WKWebView, + runJavaScriptConfirmPanelWithMessage message: String, + initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (Bool) -> Void + ) { + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet) + + alertController.addAction(UIAlertAction( + title: CoreLocalization.Webview.Alert.ok, + style: .default, + handler: { _ in + completionHandler(true) + })) + + alertController.addAction(UIAlertAction( + title: CoreLocalization.Webview.Alert.cancel, + style: .cancel, + handler: { _ in + completionHandler(false) + })) + + UIApplication.topViewController()?.present(alertController, animated: true, completion: nil) + } + + public func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction + ) async -> WKNavigationActionPolicy { guard let url = navigationAction.request.url else { return .cancel } @@ -63,12 +90,17 @@ public struct WebView: UIViewRepresentable { return .allow } - public func webView(_ webView: WKWebView, - decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy { - guard let statusCode = (navigationResponse.response as? HTTPURLResponse)?.statusCode else { + public func webView( + _ webView: WKWebView, + decidePolicyFor navigationResponse: WKNavigationResponse + ) async -> WKNavigationResponsePolicy { + guard let response = (navigationResponse.response as? HTTPURLResponse), + let url = response.url else { return .cancel } - if (401...404).contains(statusCode) { + let baseURL = await parent.viewModel.baseURL + + if (401...404).contains(response.statusCode) || url.absoluteString.hasPrefix(baseURL + "/login") { await parent.refreshCookies() DispatchQueue.main.async { if let url = webView.url { @@ -86,34 +118,35 @@ public struct WebView: UIViewRepresentable { } public func makeUIView(context: UIViewRepresentableContext) -> WKWebView { - let webview = WKWebView() - webview.navigationDelegate = context.coordinator + let webViewConfig = WKWebViewConfiguration() - webview.scrollView.bounces = false - webview.scrollView.alwaysBounceHorizontal = false - webview.scrollView.showsHorizontalScrollIndicator = false - webview.scrollView.isScrollEnabled = true - webview.configuration.suppressesIncrementalRendering = true - webview.isOpaque = false - webview.backgroundColor = .clear - webview.scrollView.backgroundColor = UIColor.clear - webview.scrollView.alwaysBounceVertical = false + let webView = WKWebView(frame: .zero, configuration: webViewConfig) + webView.navigationDelegate = context.coordinator + webView.uiDelegate = context.coordinator - return webview + webView.scrollView.bounces = false + webView.scrollView.alwaysBounceHorizontal = false + webView.scrollView.showsHorizontalScrollIndicator = false + webView.scrollView.isScrollEnabled = true + webView.configuration.suppressesIncrementalRendering = true + webView.isOpaque = false + webView.backgroundColor = .clear + webView.scrollView.backgroundColor = .white + webView.scrollView.alwaysBounceVertical = false + webView.scrollView.layer.cornerRadius = 24 + webView.scrollView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 200, right: 0) + + return webView } public func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext) { if let url = URL(string: viewModel.url) { - let cookies = HTTPCookieStorage.shared.cookies ?? [] - for (cookie) in cookies { - webview.configuration.websiteDataStore.httpCookieStore - .setCookie(cookie) - } - let request = URLRequest(url: url) if webview.url?.absoluteString != url.absoluteString { DispatchQueue.main.async { isLoading = true } + let request = URLRequest(url: url) webview.load(request) } } diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index 6fda66932..f16f781dc 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -21,6 +21,24 @@ "ERROR.UNKNOWN_ERROR" = "Something went wrong"; "ERROR.WIFI" = "You can only download files over Wi-Fi. You can change this in the settings."; +"COURSEWARE.COURSE_CONTENT" = "Course content"; +"COURSEWARE.COURSE_UNITS" = "Course units"; +"COURSEWARE.NEXT" = "Next"; +"COURSEWARE.PREVIOUS" = "Prev"; +"COURSEWARE.FINISH" = "Finish"; +"COURSEWARE.GOOD_WORK" = "Good Work!"; +"COURSEWARE.BACK_TO_OUTLINE" = "Back to outline"; +"COURSEWARE.SECTION" = "Section “"; +"COURSEWARE.IS_FINISHED" = "“ is finished."; +"COURSEWARE.CONTINUE" = "Continue"; +"COURSEWARE.CONTINUE_WITH" = "Continue with:"; +"COURSEWARE.NEXT_SECTION" = "Next section"; + +"COURSEWARE.NEXT_SECTION_DESCRIPTION_FIRST" = "To proceed with “"; +"COURSEWARE.NEXT_SECTION_DESCRIPTION_LAST" = "” press “Next section”."; + +"ERROR.RELOAD" = "Reload"; + "DATE.ENDED" = "Ended"; "DATE.START" = "Start"; "DATE.STARTED" = "Started"; @@ -47,3 +65,6 @@ "PICKER.SEARCH" = "Search"; "PICKER.ACCEPT" = "Accept"; + +"WEBVIEW.ALERT.OK" = "Ok"; +"WEBVIEW.ALERT.CANCEL" = "Cancel"; diff --git a/Core/Core/uk.lproj/Localizable.strings b/Core/Core/uk.lproj/Localizable.strings index 430249a1d..e06937311 100644 --- a/Core/Core/uk.lproj/Localizable.strings +++ b/Core/Core/uk.lproj/Localizable.strings @@ -21,6 +21,24 @@ "ERROR.UNKNOWN_ERROR" = "Щось пішло не так"; "ERROR.WIFI" = "Завантажувати файли можна лише через Wi-Fi. Ви можете змінити це в налаштуваннях."; +"COURSEWARE.COURSE_CONTENT" = "Зміст курсу"; +"COURSEWARE.COURSE_UNITS" = "Модулі"; +"COURSEWARE.NEXT" = "Далі"; +"COURSEWARE.PREVIOUS" = "Назад"; +"COURSEWARE.FINISH" = "Завершити"; +"COURSEWARE.GOOD_WORK" = "Гарна робота!"; +"COURSEWARE.BACK_TO_OUTLINE" = "Повернутись до модуля"; +"COURSEWARE.SECTION" = "Секція “"; +"COURSEWARE.IS_FINISHED" = "“ завершена."; +"COURSEWARE.CONTINUE" = "Продовжити"; +"COURSEWARE.CONTINUE_WITH" = "Продовжити далі:"; +"COURSEWARE.NEXT_SECTION" = "Наступний розділ"; + +"COURSEWARE.NEXT_SECTION_DESCRIPTION_FIRST" = "Щоб перейти до “"; +"COURSEWARE.NEXT_SECTION_DESCRIPTION_LAST" = "” натисніть “Наступний розділ”."; + +"ERROR.RELOAD" = "Перезавантажити"; + "DATE.ENDED" = "Кінець"; "DATE.START" = "Початок"; "DATE.STARTED" = "Почався"; @@ -47,3 +65,6 @@ "PICKER.SEARCH" = "Знайти"; "PICKER.ACCEPT" = "Прийняти"; + +"WEBVIEW.ALERT.OK" = "Так"; +"WEBVIEW.ALERT.CANCEL" = "Скасувати"; diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index 7c1c46947..6a9324697 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -16,15 +16,22 @@ 022C64E029ADEA9B000F532B /* Data_UpdatesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64DF29ADEA9B000F532B /* Data_UpdatesResponse.swift */; }; 022C64E229ADEB83000F532B /* CourseUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64E129ADEB83000F532B /* CourseUpdate.swift */; }; 022EA8CB297AD63B0014A8F7 /* CourseContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022EA8CA297AD63B0014A8F7 /* CourseContainerViewModelTests.swift */; }; + 022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022F8E152A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift */; }; + 022F8E182A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022F8E172A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift */; }; 0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0231124C28EDA804002588FB /* CourseUnitView.swift */; }; 0231124F28EDA811002588FB /* CourseUnitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0231124E28EDA811002588FB /* CourseUnitViewModel.swift */; }; 023812E7297AC8EB0087098F /* CourseDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023812E6297AC8EB0087098F /* CourseDetailsViewModelTests.swift */; }; 023812E8297AC8EB0087098F /* Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0289F8EE28E1C3510064F8F3 /* Course.framework */; platformFilter = ios; }; 023812F3297AC9ED0087098F /* CourseMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023812F2297AC9EC0087098F /* CourseMock.generated.swift */; }; - 0248C92529C0901200DC8402 /* CourseBlocksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248C92429C0901200DC8402 /* CourseBlocksViewModel.swift */; }; + 02454CA02A2618E70043052A /* YouTubeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454C9F2A2618E70043052A /* YouTubeView.swift */; }; + 02454CA22A26190A0043052A /* EncodedVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA12A26190A0043052A /* EncodedVideoView.swift */; }; + 02454CA42A26193F0043052A /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA32A26193F0043052A /* WebView.swift */; }; + 02454CA62A26196C0043052A /* UnknownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA52A26196C0043052A /* UnknownView.swift */; }; + 02454CA82A2619890043052A /* DiscussionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA72A2619890043052A /* DiscussionView.swift */; }; + 02454CAA2A2619B40043052A /* LessonProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA92A2619B40043052A /* LessonProgressView.swift */; }; 0248C92729C097EB00DC8402 /* CourseVerticalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */; }; - 02512FEE298EAD770024D438 /* CourseBlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02512FED298EAD770024D438 /* CourseBlocksView.swift */; }; 0262148F29AE17C4008BD75A /* HandoutsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0262148E29AE17C4008BD75A /* HandoutsViewModelTests.swift */; }; + 02635AC72A24F181008062F2 /* ContinueWithView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02635AC62A24F181008062F2 /* ContinueWithView.swift */; }; 0265B4B728E2141D00E6EAFD /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0265B4B628E2141D00E6EAFD /* Strings.swift */; }; 027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027020FB28E7362100F54332 /* Data_CourseOutlineResponse.swift */; }; 0270210328E736E700F54332 /* CourseOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0270210128E736E700F54332 /* CourseOutlineView.swift */; }; @@ -32,7 +39,6 @@ 0276D75D29DDA3F80004CDF8 /* ResumeBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */; }; 0289F90228E1C3E10064F8F3 /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = 0289F90128E1C3E00064F8F3 /* swiftgen.yml */; }; 0295B1D9297E6DF8003B0C65 /* CourseUnitViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295B1D8297E6DF8003B0C65 /* CourseUnitViewModelTests.swift */; }; - 0295C887299BBDE300ABE571 /* UnitButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295C886299BBDE300ABE571 /* UnitButtonView.swift */; }; 0295C889299BBE8200ABE571 /* CourseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295C888299BBE8200ABE571 /* CourseNavigationView.swift */; }; 02A8076829474831007F53AB /* CourseVerticalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A8076729474831007F53AB /* CourseVerticalView.swift */; }; 02B6B3B228E1C49400232911 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 02B6B3B428E1C49400232911 /* Localizable.strings */; }; @@ -79,15 +85,22 @@ 022C64DF29ADEA9B000F532B /* Data_UpdatesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_UpdatesResponse.swift; sourceTree = ""; }; 022C64E129ADEB83000F532B /* CourseUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUpdate.swift; sourceTree = ""; }; 022EA8CA297AD63B0014A8F7 /* CourseContainerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseContainerViewModelTests.swift; sourceTree = ""; }; + 022F8E152A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeVideoPlayerViewModel.swift; sourceTree = ""; }; + 022F8E172A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodedVideoPlayerViewModel.swift; sourceTree = ""; }; 0231124C28EDA804002588FB /* CourseUnitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUnitView.swift; sourceTree = ""; }; 0231124E28EDA811002588FB /* CourseUnitViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUnitViewModel.swift; sourceTree = ""; }; 023812E4297AC8EA0087098F /* CourseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CourseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 023812E6297AC8EB0087098F /* CourseDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDetailsViewModelTests.swift; sourceTree = ""; }; 023812F2297AC9EC0087098F /* CourseMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseMock.generated.swift; sourceTree = ""; }; - 0248C92429C0901200DC8402 /* CourseBlocksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseBlocksViewModel.swift; sourceTree = ""; }; + 02454C9F2A2618E70043052A /* YouTubeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeView.swift; sourceTree = ""; }; + 02454CA12A26190A0043052A /* EncodedVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodedVideoView.swift; sourceTree = ""; }; + 02454CA32A26193F0043052A /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; + 02454CA52A26196C0043052A /* UnknownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownView.swift; sourceTree = ""; }; + 02454CA72A2619890043052A /* DiscussionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionView.swift; sourceTree = ""; }; + 02454CA92A2619B40043052A /* LessonProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonProgressView.swift; sourceTree = ""; }; 0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVerticalViewModel.swift; sourceTree = ""; }; - 02512FED298EAD770024D438 /* CourseBlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseBlocksView.swift; sourceTree = ""; }; 0262148E29AE17C4008BD75A /* HandoutsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoutsViewModelTests.swift; sourceTree = ""; }; + 02635AC62A24F181008062F2 /* ContinueWithView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWithView.swift; sourceTree = ""; }; 0265B4B628E2141D00E6EAFD /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 027020FB28E7362100F54332 /* Data_CourseOutlineResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseOutlineResponse.swift; sourceTree = ""; }; 0270210128E736E700F54332 /* CourseOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseOutlineView.swift; sourceTree = ""; }; @@ -96,7 +109,6 @@ 0289F8EE28E1C3510064F8F3 /* Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Course.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0289F90128E1C3E00064F8F3 /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; 0295B1D8297E6DF8003B0C65 /* CourseUnitViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUnitViewModelTests.swift; sourceTree = ""; }; - 0295C886299BBDE300ABE571 /* UnitButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitButtonView.swift; sourceTree = ""; }; 0295C888299BBE8200ABE571 /* CourseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseNavigationView.swift; sourceTree = ""; }; 02A8076729474831007F53AB /* CourseVerticalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVerticalView.swift; sourceTree = ""; }; 02B6B3B328E1C49400232911 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -187,6 +199,19 @@ path = CourseTests; sourceTree = ""; }; + 02454C9E2A2618D40043052A /* Subviews */ = { + isa = PBXGroup; + children = ( + 02454C9F2A2618E70043052A /* YouTubeView.swift */, + 02454CA12A26190A0043052A /* EncodedVideoView.swift */, + 02454CA32A26193F0043052A /* WebView.swift */, + 02454CA52A26196C0043052A /* UnknownView.swift */, + 02454CA72A2619890043052A /* DiscussionView.swift */, + 02454CA92A2619B40043052A /* LessonProgressView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; 0289F8E428E1C3510064F8F3 = { isa = PBXGroup; children = ( @@ -325,11 +350,10 @@ 070019A728F6F2D600D5FC78 /* Outline */ = { isa = PBXGroup; children = ( + 02635AC62A24F181008062F2 /* ContinueWithView.swift */, 0270210128E736E700F54332 /* CourseOutlineView.swift */, 02A8076729474831007F53AB /* CourseVerticalView.swift */, 0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */, - 02512FED298EAD770024D438 /* CourseBlocksView.swift */, - 0248C92429C0901200DC8402 /* CourseBlocksViewModel.swift */, ); path = Outline; sourceTree = ""; @@ -347,10 +371,10 @@ 070019A928F6F59D00D5FC78 /* Unit */ = { isa = PBXGroup; children = ( + 02454C9E2A2618D40043052A /* Subviews */, 0231124C28EDA804002588FB /* CourseUnitView.swift */, 0231124E28EDA811002588FB /* CourseUnitViewModel.swift */, 0295C888299BBE8200ABE571 /* CourseNavigationView.swift */, - 0295C886299BBDE300ABE571 /* UnitButtonView.swift */, ); path = Unit; sourceTree = ""; @@ -360,7 +384,9 @@ children = ( 02F066E729DC71750073E13B /* SubtittlesView.swift */, 0766DFCB299AA7A600EBEF6A /* YouTubeVideoPlayer.swift */, + 022F8E152A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift */, 0766DFCD299AB26D00EBEF6A /* EncodedVideoPlayer.swift */, + 022F8E172A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift */, 0766DFCF299AB29000EBEF6A /* PlayerViewController.swift */, 02FFAD0C29E4347300140E46 /* VideoPlayerViewModel.swift */, ); @@ -645,39 +671,45 @@ buildActionMask = 2147483647; files = ( 02FFAD0D29E4347300140E46 /* VideoPlayerViewModel.swift in Sources */, + 02454CA42A26193F0043052A /* WebView.swift in Sources */, 022C64DA29ACEC50000F532B /* HandoutsViewModel.swift in Sources */, + 02635AC72A24F181008062F2 /* ContinueWithView.swift in Sources */, 022C64DE29AD167A000F532B /* HandoutsUpdatesDetailView.swift in Sources */, 0270210328E736E700F54332 /* CourseOutlineView.swift in Sources */, 022C64E029ADEA9B000F532B /* Data_UpdatesResponse.swift in Sources */, + 02454CA02A2618E70043052A /* YouTubeView.swift in Sources */, + 02454CA22A26190A0043052A /* EncodedVideoView.swift in Sources */, 02B6B3BC28E1D14F00232911 /* CourseRepository.swift in Sources */, 02280F60294B50030032823A /* CoursePersistence.swift in Sources */, + 02454CAA2A2619B40043052A /* LessonProgressView.swift in Sources */, 02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */, 0766DFCE299AB26D00EBEF6A /* EncodedVideoPlayer.swift in Sources */, 0276D75B29DDA3890004CDF8 /* Data_ResumeBlock.swift in Sources */, 0276D75D29DDA3F80004CDF8 /* ResumeBlock.swift in Sources */, 02F3BFDD29252E900051930C /* CourseRouter.swift in Sources */, + 022F8E182A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift in Sources */, 0248C92729C097EB00DC8402 /* CourseVerticalViewModel.swift in Sources */, - 0295C887299BBDE300ABE571 /* UnitButtonView.swift in Sources */, 02F0145728F4A2FF002E513D /* CourseContainerViewModel.swift in Sources */, 02B6B3B728E1D11E00232911 /* CourseInteractor.swift in Sources */, 073512E229C0E400005CFA41 /* BaseCourseViewModel.swift in Sources */, - 02512FEE298EAD770024D438 /* CourseBlocksView.swift in Sources */, 0231124F28EDA811002588FB /* CourseUnitViewModel.swift in Sources */, 02F0144F28F46474002E513D /* CourseContainerView.swift in Sources */, 02A8076829474831007F53AB /* CourseVerticalView.swift in Sources */, - 0248C92529C0901200DC8402 /* CourseBlocksViewModel.swift in Sources */, 0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */, 027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */, 02E685C028E4B629000AE015 /* CourseDetailsViewModel.swift in Sources */, 0295C889299BBE8200ABE571 /* CourseNavigationView.swift in Sources */, 02F066E829DC71750073E13B /* SubtittlesView.swift in Sources */, 022C64E229ADEB83000F532B /* CourseUpdate.swift in Sources */, + 02454CA62A26196C0043052A /* UnknownView.swift in Sources */, 0766DFD0299AB29000EBEF6A /* PlayerViewController.swift in Sources */, 022C64DC29ACFDEE000F532B /* Data_HandoutsResponse.swift in Sources */, 022C64D829ACEC48000F532B /* HandoutsView.swift in Sources */, + 02454CA82A2619890043052A /* DiscussionView.swift in Sources */, 0265B4B728E2141D00E6EAFD /* Strings.swift in Sources */, 02B6B3C128E1DBA100232911 /* Data_CourseDetailsResponse.swift in Sources */, 0766DFCC299AA7A600EBEF6A /* YouTubeVideoPlayer.swift in Sources */, + 022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */, 02E685BE28E4B60A000AE015 /* CourseDetailsView.swift in Sources */, 02B6B3BE28E1D15C00232911 /* CourseDetailsEndpoint.swift in Sources */, 02B6B3C328E1DCD100232911 /* CourseDetails.swift in Sources */, @@ -714,7 +746,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -735,7 +767,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -756,7 +788,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -777,7 +809,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -798,7 +830,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -819,7 +851,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -1512,7 +1544,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -1625,7 +1657,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index e7cbcd9e7..30f3555df 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -18,7 +18,7 @@ public protocol CourseRepositoryProtocol { func getHandouts(courseID: String) async throws -> String? func getUpdates(courseID: String) async throws -> [CourseUpdate] func resumeBlock(courseID: String) async throws -> ResumeBlock - func getSubtitles(url: String) async throws -> String + func getSubtitles(url: String, selectedLanguage: String) async throws -> String } public class CourseRepository: CourseRepositoryProtocol { @@ -98,13 +98,16 @@ public class CourseRepository: CourseRepositoryProtocol { .mapResponse(DataLayer.ResumeBlock.self).domain } - public func getSubtitles(url: String) async throws -> String { - if let subtitlesOffline = persistence.loadSubtitles(url: url) { + public func getSubtitles(url: String, selectedLanguage: String) async throws -> String { + if let subtitlesOffline = persistence.loadSubtitles(url: url + selectedLanguage) { return subtitlesOffline } else { - let result = try await api.requestData(CourseDetailsEndpoint.getSubtitles(url: url)) + let result = try await api.requestData(CourseDetailsEndpoint.getSubtitles( + url: url, + selectedLanguage: selectedLanguage + )) let subtitles = String(data: result, encoding: .utf8) ?? "" - persistence.saveSubtitles(url: url, subtitlesString: subtitles) + persistence.saveSubtitles(url: url + selectedLanguage, subtitlesString: subtitles) return subtitles } } @@ -241,7 +244,7 @@ class CourseRepositoryMock: CourseRepositoryProtocol { let decoder = JSONDecoder() let jsonData = Data(courseStructureJson.utf8) let courseBlocks = try decoder.decode(DataLayer.CourseStructure.self, from: jsonData) - return parseCourseStructure(courseBlocks: courseBlocks) + return parseCourseStructure(structure: courseBlocks) } public func getCourseDetails(courseID: String) async throws -> CourseDetails { @@ -263,10 +266,12 @@ class CourseRepositoryMock: CourseRepositoryProtocol { public func getCourseBlocks(courseID: String) async throws -> CourseStructure { do { - let decoder = JSONDecoder() - let jsonData = Data(courseStructureJson.utf8) - let courseBlocks = try decoder.decode(DataLayer.CourseStructure.self, from: jsonData) - return parseCourseStructure(courseBlocks: courseBlocks) +// let decoder = JSONDecoder() +// let jsonData = Data(courseStructureJson.utf8) + let courseBlocks = try courseStructureJson.data(using: .utf8)!.mapResponse(DataLayer.CourseStructure.self) + +// let courseBlocks = try decoder.decode(DataLayer.CourseStructure.self, from: jsonData) + return parseCourseStructure(structure: courseBlocks) } catch { throw error } @@ -280,7 +285,7 @@ class CourseRepositoryMock: CourseRepositoryProtocol { } - public func getSubtitles(url: String) async throws -> String { + public func getSubtitles(url: String, selectedLanguage: String) async throws -> String { return """ 0 00:00:00,350 --> 00:00:05,230 @@ -303,8 +308,8 @@ And there are various ways of describing it-- call it oral poetry or """ } - private func parseCourseStructure(courseBlocks: DataLayer.CourseStructure) -> CourseStructure { - let blocks = Array(courseBlocks.dict.values) + private func parseCourseStructure(structure: DataLayer.CourseStructure) -> CourseStructure { + let blocks = Array(structure.dict.values) let course = blocks.first(where: {$0.type == BlockType.course.rawValue })! let descendants = course.descendants ?? [] var childs: [CourseChapter] = [] @@ -321,8 +326,8 @@ And there are various ways of describing it-- call it oral poetry or displayName: course.displayName, topicID: course.userViewData?.topicID, childs: childs, - media: courseBlocks.media, - certificate: courseBlocks.certificate?.domain) + media: structure.media, + certificate: structure.certificate?.domain) } private func parseChapters(id: String, blocks: [DataLayer.CourseBlock]) -> CourseChapter { @@ -375,11 +380,12 @@ And there are various ways of describing it-- call it oral poetry or private func parseBlock(id: String, blocks: [DataLayer.CourseBlock]) -> CourseBlock { let block = blocks.first(where: {$0.id == id })! let subtitles = block.userViewData?.transcripts?.map { -// let url = $0.value + let url = $0.value // .replacingOccurrences(of: config.baseURL.absoluteString, with: "") -// .replacingOccurrences(of: "?lang=en", with: "") - SubtitleUrl(language: $0.key, url: $0.value) +// .replacingOccurrences(of: "?lang=\($0.key)", with: "") + return SubtitleUrl(language: $0.key, url: url) } + return CourseBlock(blockId: block.blockId, id: block.id, topicId: block.userViewData?.topicID, @@ -393,443 +399,627 @@ And there are various ways of describing it-- call it oral poetry or youTubeUrl: block.userViewData?.encodedVideo?.youTube?.url) } - private let courseStructureJson: String = "{\n" + - " \"root\": \"block-v1:RG+MC01+2022+type@course+block@course\",\n" + - " \"blocks\": {\n" + - " \"block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139\",\n" + - " \"block_id\": \"8718fdf95d584d198a3b17c0d2611139\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139\",\n" + - " \"type\": \"html\",\n" + - " \"display_name\": \"Text\",\n" + - " \"graded\": false,\n" + - " \"student_view_data\": {\n" + - " \"enabled\": false,\n" + - " \"message\": \"To enable, set FEATURES[\\\"ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA\\\"]\"\n" + - " },\n" + - " \"student_view_multi_device\": true,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [],\n" + - " \"completion\": 1.0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a\",\n" + - " \"block_id\": \"d1bb8c9e6ed44b708ea54cacf67b650a\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a\",\n" + - " \"type\": \"video\",\n" + - " \"display_name\": \"Video\",\n" + - " \"graded\": false,\n" + - " \"student_view_data\": {\n" + - " \"only_on_web\": false,\n" + - " \"duration\": null,\n" + - " \"transcripts\": {\n" + - " \"en\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/xblock/block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a/handler_noauth/transcript/download?lang=en\"\n" + - " },\n" + - " \"encoded_videos\": {\n" + - " \"youtube\": {\n" + - " \"url\": \"https://www.youtube.com/watch?v=3_yD_cEKoCk\",\n" + - " \"file_size\": 0\n" + - " }\n" + - " },\n" + - " \"all_sources\": []\n" + - " },\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 1\n" + - " },\n" + - " \"descendants\": [],\n" + - " \"completion\": 0.0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8\",\n" + - " \"block_id\": \"8ccbceb2abec4028a9cc8b1fecf5e7d8\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8\",\n" + - " \"type\": \"vertical\",\n" + - " \"display_name\": \"Welcome!\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 1\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@html+block@8718fdf95d584d198a3b17c0d2611139\",\n" + - " \"block-v1:RG+MC01+2022+type@video+block@d1bb8c9e6ed44b708ea54cacf67b650a\"\n" + - " ],\n" + - " \"completion\": 0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4\",\n" + - " \"block_id\": \"5735347ae4be44d5b184728661d79bb4\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4\",\n" + - " \"type\": \"html\",\n" + - " \"display_name\": \"Text\",\n" + - " \"graded\": false,\n" + - " \"student_view_data\": {\n" + - " \"enabled\": false,\n" + - " \"message\": \"To enable, set FEATURES[\\\"ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA\\\"]\"\n" + - " },\n" + - " \"student_view_multi_device\": true,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [],\n" + - " \"completion\": 1.0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27\",\n" + - " \"block_id\": \"0b26805b246c44148a2c02dfbffa2b27\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27\",\n" + - " \"type\": \"discussion\",\n" + - " \"display_name\": \"Discussion\",\n" + - " \"graded\": false,\n" + - " \"student_view_data\": {\n" + - " \"topic_id\": \"035315aac3f889b472c8f051d8fd0abaa99682de\"\n" + - " },\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": []\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98\",\n" + - " \"block_id\": \"890277efe17a42a185c68b8ba8fc5a98\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98\",\n" + - " \"type\": \"vertical\",\n" + - " \"display_name\": \"General Info\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@html+block@5735347ae4be44d5b184728661d79bb4\",\n" + - " \"block-v1:RG+MC01+2022+type@discussion+block@0b26805b246c44148a2c02dfbffa2b27\"\n" + - " ],\n" + - " \"completion\": 1\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883\",\n" + - " \"block_id\": \"45b174bf007b4d86a3a265d996565883\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883\",\n" + - " \"type\": \"sequential\",\n" + - " \"display_name\": \"Course Intro\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 1\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@vertical+block@8ccbceb2abec4028a9cc8b1fecf5e7d8\",\n" + - " \"block-v1:RG+MC01+2022+type@vertical+block@890277efe17a42a185c68b8ba8fc5a98\"\n" + - " ],\n" + - " \"completion\": 0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7\",\n" + - " \"block_id\": \"7cb5739b6ead4fc39b126bbe56cdb9c7\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7\",\n" + - " \"type\": \"chapter\",\n" + - " \"display_name\": \"Info Section\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 1\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@sequential+block@45b174bf007b4d86a3a265d996565883\"\n" + - " ],\n" + - " \"completion\": 0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be\",\n" + - " \"block_id\": \"376ec419a01449fd86c2d11c8054d0be\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be\",\n" + - " \"type\": \"problem\",\n" + - " \"display_name\": \"Checkboxes\",\n" + - " \"graded\": true,\n" + - " \"student_view_multi_device\": true,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [],\n" + - " \"completion\": 1.0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf\",\n" + - " \"block_id\": \"ebc2d20fad364992b13fff49fc53d7cf\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf\",\n" + - " \"type\": \"problem\",\n" + - " \"display_name\": \"Dropdown\",\n" + - " \"graded\": true,\n" + - " \"student_view_multi_device\": true,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [],\n" + - " \"completion\": 1.0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b\",\n" + - " \"block_id\": \"6b822c82f2ca4b049ee380a1cf65396b\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b\",\n" + - " \"type\": \"problem\",\n" + - " \"display_name\": \"Numerical Input with Hints and Feedback\",\n" + - " \"graded\": true,\n" + - " \"student_view_multi_device\": true,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [],\n" + - " \"completion\": 1.0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863\",\n" + - " \"block_id\": \"009da5f764a04078855d322e205c5863\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863\",\n" + - " \"type\": \"problem\",\n" + - " \"display_name\": \"Multiple Choice\",\n" + - " \"graded\": true,\n" + - " \"student_view_multi_device\": true,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [],\n" + - " \"completion\": 1.0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4\",\n" + - " \"block_id\": \"e34d9616cbaa45d1a6986a687c49f5c4\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4\",\n" + - " \"type\": \"vertical\",\n" + - " \"display_name\": \"Common Problems\",\n" + - " \"graded\": true,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@problem+block@376ec419a01449fd86c2d11c8054d0be\",\n" + - " \"block-v1:RG+MC01+2022+type@problem+block@ebc2d20fad364992b13fff49fc53d7cf\",\n" + - " \"block-v1:RG+MC01+2022+type@problem+block@6b822c82f2ca4b049ee380a1cf65396b\",\n" + - " \"block-v1:RG+MC01+2022+type@problem+block@009da5f764a04078855d322e205c5863\"\n" + - " ],\n" + - " \"completion\": 1\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56\",\n" + - " \"block_id\": \"ac7862e8c3c9481bbe657a82795def56\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56\",\n" + - " \"type\": \"sequential\",\n" + - " \"display_name\": \"Test\",\n" + - " \"graded\": true,\n" + - " \"format\": \"Final Exam\",\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@vertical+block@e34d9616cbaa45d1a6986a687c49f5c4\"\n" + - " ],\n" + - " \"completion\": 1\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d\",\n" + - " \"block_id\": \"9355144723fc4270a1081547fd8bdd3d\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d\",\n" + - " \"type\": \"problem\",\n" + - " \"display_name\": \"Image Mapped Input\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [],\n" + - " \"completion\": 0.0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340\",\n" + - " \"block_id\": \"50d42e9c9d91451fb50693e01b9e4340\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340\",\n" + - " \"type\": \"vertical\",\n" + - " \"display_name\": \"Advanced Problems\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@problem+block@9355144723fc4270a1081547fd8bdd3d\"\n" + - " ],\n" + - " \"completion\": 0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be\",\n" + - " \"block_id\": \"5cdb10d7d0e9498faba55450173e23be\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be\",\n" + - " \"type\": \"sequential\",\n" + - " \"display_name\": \"X-blocks not supported in app\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@vertical+block@50d42e9c9d91451fb50693e01b9e4340\"\n" + - " ],\n" + - " \"completion\": 0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a\",\n" + - " \"block_id\": \"8f208b5d63234ce483f7d6702c46238a\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a\",\n" + - " \"type\": \"chapter\",\n" + - " \"display_name\": \"Problems\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@sequential+block@ac7862e8c3c9481bbe657a82795def56\",\n" + - " \"block-v1:RG+MC01+2022+type@sequential+block@5cdb10d7d0e9498faba55450173e23be\"\n" + - " ],\n" + - " \"completion\": 0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2\",\n" + - " \"block_id\": \"4b0ea9edbf59484fb5ecc1f8f29f73c2\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2\",\n" + - " \"type\": \"html\",\n" + - " \"display_name\": \"Text\",\n" + - " \"graded\": false,\n" + - " \"student_view_data\": {\n" + - " \"enabled\": false,\n" + - " \"message\": \"To enable, set FEATURES[\\\"ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA\\\"]\"\n" + - " },\n" + - " \"student_view_multi_device\": true,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [],\n" + - " \"completion\": 1.0\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1\",\n" + - " \"block_id\": \"b912f9ba42ac43c492bfb423e15b0da1\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1\",\n" + - " \"type\": \"vertical\",\n" + - " \"display_name\": \"Thank you\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@html+block@4b0ea9edbf59484fb5ecc1f8f29f73c2\"\n" + - " ],\n" + - " \"completion\": 1\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9\",\n" + - " \"block_id\": \"a6e2101867234019b60607a9b9bf64f9\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9\",\n" + - " \"type\": \"sequential\",\n" + - " \"display_name\": \"Thank you note\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@vertical+block@b912f9ba42ac43c492bfb423e15b0da1\"\n" + - " ],\n" + - " \"completion\": 1\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222\",\n" + - " \"block_id\": \"29f4043d199e46ef95d437da3be1d222\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222\",\n" + - " \"type\": \"chapter\",\n" + - " \"display_name\": \"Fin\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 0\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@sequential+block@a6e2101867234019b60607a9b9bf64f9\"\n" + - " ],\n" + - " \"completion\": 1\n" + - " },\n" + - " \"block-v1:RG+MC01+2022+type@course+block@course\": {\n" + - " \"id\": \"block-v1:RG+MC01+2022+type@course+block@course\",\n" + - " \"block_id\": \"course\",\n" + - " \"lms_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@course+block@course\",\n" + - " \"legacy_web_url\": \"https://lms-client-demo-maple.raccoongang.com/courses/course-v1:RG+MC01+2022/jump_to/block-v1:RG+MC01+2022+type@course+block@course?experience=legacy\",\n" + - " \"student_view_url\": \"https://lms-client-demo-maple.raccoongang.com/xblock/block-v1:RG+MC01+2022+type@course+block@course\",\n" + - " \"type\": \"course\",\n" + - " \"display_name\": \"Mobile Course Demo\",\n" + - " \"graded\": false,\n" + - " \"student_view_multi_device\": false,\n" + - " \"block_counts\": {\n" + - " \"video\": 1\n" + - " },\n" + - " \"descendants\": [\n" + - " \"block-v1:RG+MC01+2022+type@chapter+block@7cb5739b6ead4fc39b126bbe56cdb9c7\",\n" + - " \"block-v1:RG+MC01+2022+type@chapter+block@8f208b5d63234ce483f7d6702c46238a\",\n" + - " \"block-v1:RG+MC01+2022+type@chapter+block@29f4043d199e46ef95d437da3be1d222\"\n" + - " ],\n" + - " \"completion\": 0\n" + - " }\n" + - " }\n" + - "}" + private let courseStructureJson: String = """ + {"root": "block-v1:QA+comparison+2022+type@course+block@course", + "blocks": { + "block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78": { + "id": "block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78", + "block_id": "be1704c576284ba39753c6f0ea4a4c78", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78", + "type": "comparison", + "display_name": "Співставлення", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296": { + "id": "block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296", + "block_id": "93acc543871e4c73bc20a72a64e93296", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296", + "type": "problem", + "display_name": "Dropdown with Hints and Feedback", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b": { + "id": "block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b", + "block_id": "06c17035106e48328ebcd042babcf47b", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b", + "type": "comparison", + "display_name": "Співставлення", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58": { + "id": "block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58", + "block_id": "c19e41b61db14efe9c45f1354332ae58", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58", + "type": "problem", + "display_name": "Text Input with Hints and Feedback", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb": { + "id": "block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb", + "block_id": "0d96732f577b4ff68799faf8235d1bfb", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb", + "type": "problem", + "display_name": "Numerical Input with Hints and Feedback", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96": { + "id": "block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96", + "block_id": "dd2e22fdf0724bd88c8b2e6b68dedd96", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96", + "type": "problem", + "display_name": "Blank Common Problem", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd": { + "id": "block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd", + "block_id": "d1e091aa305741c5bedfafed0d269efd", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd", + "type": "problem", + "display_name": "Blank Common Problem", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7": { + "id": "block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7", + "block_id": "23e10dea806345b19b77997b4fc0eea7", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7", + "type": "comparison", + "display_name": "Співставлення", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904": { + "id": "block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904", + "block_id": "29e7eddbe8964770896e4036748c9904", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904", + "type": "vertical", + "display_name": "Юніт", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78", + "block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296", + "block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b", + "block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58", + "block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb", + "block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96", + "block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd", + "block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6": { + "id": "block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6", + "block_id": "f468bb5c6e8641179e523c7fcec4e6d6", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6", + "type": "sequential", + "display_name": "Підрозділ", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234": { + "id": "block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234", + "block_id": "eaf91d8fc70547339402043ba1a1c234", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234", + "type": "problem", + "display_name": "Dropdown with Hints and Feedback", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751": { + "id": "block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751", + "block_id": "fac531c3f1f3400cb8e3b97eb2c3d751", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751", + "type": "comparison", + "display_name": "Comparison", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de": { + "id": "block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de", + "block_id": "74a1074024fe401ea305534f2241e5de", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de", + "type": "html", + "display_name": "Raw HTML", + "graded": false, + "student_view_data": { + "last_modified": "2023-05-04T19:08:07Z", + "html_data": "https://s3.eu-central-1.amazonaws.com/vso-dev-edx-sorage/htmlxblock/QA/comparison/html/74a1074024fe401ea305534f2241e5de/content_html.zip", + "size": 576, + "index_page": "index.html", + "icon_class": "other" + }, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca": { + "id": "block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca", + "block_id": "e5b2e105f4f947c5b76fb12c35da1eca", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca", + "type": "vertical", + "display_name": "Юніт", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234", + "block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751", + "block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2": { + "id": "block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2", + "block_id": "d37cb0c5c2d24ddaacf3494760a055f2", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2", + "type": "sequential", + "display_name": "Ще один Підрозділ", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846": { + "id": "block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846", + "block_id": "abecaefe203c4c93b441d16cea3b7846", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846", + "type": "chapter", + "display_name": "Розділ", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6", + "block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de", + "block_id": "a0c3ac29daab425f92a34b34eb2af9de", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de", + "type": "pdf", + "display_name": "PDF файл заголовок", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9", + "block_id": "bcd1b0f3015b4d3696b12f65a5d682f9", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9", + "type": "pdf", + "display_name": "PDF", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59", + "block_id": "67d805daade34bd4b6ace607e6d48f59", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59", + "type": "pdf", + "display_name": "PDF", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974", + "block_id": "828606a51f4e44198e92f86a45be7974", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974", + "type": "pdf", + "display_name": "PDF", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee", + "block_id": "8646c3bc2184467b86e5ef01ecd452ee", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee", + "type": "pdf", + "display_name": "PDF", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1": { + "id": "block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1", + "block_id": "e2faa0e62223489e91a41700865c5fc1", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1", + "type": "vertical", + "display_name": "Юніт", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de", + "block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9", + "block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59", + "block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974", + "block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52": { + "id": "block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52", + "block_id": "0c5e89fa6d7a4fac8f7b26f2ca0bbe52", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52", + "type": "problem", + "display_name": "Checkboxes with Hints and Feedback", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4": { + "id": "block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4", + "block_id": "8ba437d8b20d416d91a2d362b0c940a4", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4", + "type": "vertical", + "display_name": "Юніт", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d", + "block_id": "021f70794f7349998e190b060260b70d", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d", + "type": "pdf", + "display_name": "PDF", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1": { + "id": "block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1", + "block_id": "2c344115d3554ac58c140ec86e591aa1", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1", + "type": "vertical", + "display_name": "Юніт", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe": { + "id": "block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe", + "block_id": "6c9c6ba663b54c0eb9cbdcd0c6b4bebe", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe", + "type": "sequential", + "display_name": "Підрозділ", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1", + "block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4", + "block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7": { + "id": "block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7", + "block_id": "d5a4f1f2f5314288aae400c270fb03f7", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7", + "type": "chapter", + "display_name": "PDF", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf": { + "id": "block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf", + "block_id": "7ab45affb80f4846a60648ec6aff9fbf", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf", + "type": "chapter", + "display_name": "Розділ", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ] + }, + "block-v1:QA+comparison+2022+type@course+block@course": { + "id": "block-v1:QA+comparison+2022+type@course+block@course", + "block_id": "course", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@course+block@course", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@course+block@course?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@course+block@course", + "type": "course", + "display_name": "Comparison xblock test coursre", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846", + "block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7", + "block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf" + ], + "completion": 0 + } + }, + "id": "course-v1:QA+comparison+2022", + "name": "Comparison xblock test coursre", + "number": "comparison", + "org": "QA", + "start": "2022-01-01T00:00:00Z", + "start_display": "01 січня 2022 р.", + "start_type": "timestamp", + "end": null, + "courseware_access": { + "has_access": true, + "error_code": null, + "developer_message": null, + "user_message": null, + "additional_context_user_message": null, + "user_fragment": null + }, + "media": { + "image": { + "raw": "/asset-v1:QA+comparison+2022+type@asset+block@images_course_image.jpg", + "small": "/asset-v1:QA+comparison+2022+type@asset+block@images_course_image.jpg", + "large": "/asset-v1:QA+comparison+2022+type@asset+block@images_course_image.jpg" + } + }, + "certificate": { + + }, + "is_self_paced": false + } + """ } #endif diff --git a/Course/Course/Data/Network/CourseDetailsEndpoint.swift b/Course/Course/Data/Network/CourseDetailsEndpoint.swift index 3207fc4ff..57bb2459f 100644 --- a/Course/Course/Data/Network/CourseDetailsEndpoint.swift +++ b/Course/Course/Data/Network/CourseDetailsEndpoint.swift @@ -18,7 +18,7 @@ enum CourseDetailsEndpoint: EndPointType { case getHandouts(courseID: String) case getUpdates(courseID: String) case resumeBlock(userName: String, courseID: String) - case getSubtitles(url: String) + case getSubtitles(url: String, selectedLanguage: String) var path: String { switch self { @@ -38,7 +38,7 @@ enum CourseDetailsEndpoint: EndPointType { return "/api/mobile/v1/course_info/\(courseID)/updates" case let .resumeBlock(userName, courseID): return "/api/mobile/v1/users/\(userName)/course_status_info/\(courseID)" - case .getSubtitles(url: let url): + case let .getSubtitles(url, _): return url } } @@ -111,10 +111,10 @@ enum CourseDetailsEndpoint: EndPointType { return .requestParameters(encoding: JSONEncoding.default) case .resumeBlock: return .requestParameters(encoding: JSONEncoding.default) - case .getSubtitles: - let languageCode = Locale.current.languageCode ?? "en" + case let .getSubtitles(_, subtitleLanguage): +// let languageCode = Locale.current.languageCode ?? "en" let params: [String: Any] = [ - "lang": languageCode + "lang": subtitleLanguage ] return .requestParameters(parameters: params, encoding: URLEncoding.queryString) } diff --git a/Course/Course/Data/Persistence/CoursePersistence.swift b/Course/Course/Data/Persistence/CoursePersistence.swift index 2d5531b00..1a9731e12 100644 --- a/Course/Course/Data/Persistence/CoursePersistence.swift +++ b/Course/Course/Data/Persistence/CoursePersistence.swift @@ -156,14 +156,19 @@ public class CoursePersistence: CoursePersistenceProtocol { result[block.id] = block } ?? [:] - return DataLayer.CourseStructure(rootItem: structure.rootItem ?? "", - dict: dictionary, - id: structure.id ?? "", - media: DataLayer.CourseMedia(image: - DataLayer.Image(raw: structure.mediaRaw ?? "", - small: structure.mediaSmall ?? "", - large: structure.mediaLarge ?? "")), - certificate: DataLayer.Certificate(url: structure.certificate)) + return DataLayer.CourseStructure( + rootItem: structure.rootItem ?? "", + dict: dictionary, + id: structure.id ?? "", + media: DataLayer.CourseMedia( + image: DataLayer.Image( + raw: structure.mediaRaw ?? "", + small: structure.mediaSmall ?? "", + large: structure.mediaLarge ?? "" + ) + ), + certificate: DataLayer.Certificate(url: structure.certificate) + ) } public func saveCourseStructure(structure: DataLayer.CourseStructure) { diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift index 87eda8281..abdbd40d4 100644 --- a/Course/Course/Domain/CourseInteractor.swift +++ b/Course/Course/Domain/CourseInteractor.swift @@ -20,7 +20,7 @@ public protocol CourseInteractorProtocol { func getHandouts(courseID: String) async throws -> String? func getUpdates(courseID: String) async throws -> [CourseUpdate] func resumeBlock(courseID: String) async throws -> ResumeBlock - func getSubtitles(url: String) async throws -> [Subtitle] + func getSubtitles(url: String, selectedLanguage: String) async throws -> [Subtitle] } public class CourseInteractor: CourseInteractorProtocol { @@ -89,8 +89,8 @@ public class CourseInteractor: CourseInteractorProtocol { return try await repository.resumeBlock(courseID: courseID) } - public func getSubtitles(url: String) async throws -> [Subtitle] { - let result = try await repository.getSubtitles(url: url) + public func getSubtitles(url: String, selectedLanguage: String) async throws -> [Subtitle] { + let result = try await repository.getSubtitles(url: url, selectedLanguage: selectedLanguage) return parseSubtitles(from: result) } diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 78dd8ca7f..9c698850c 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -98,6 +98,11 @@ public struct CourseContainerView: View { .introspectViewController { vc in vc.navigationController?.setNavigationBarHidden(true, animated: false) } + .onFirstAppear { + Task { + await viewModel.tryToRefreshCookies() + } + } } } } @@ -109,6 +114,7 @@ struct CourseScreensView_Previews: PreviewProvider { CourseContainerView( viewModel: CourseContainerViewModel( interactor: CourseInteractor.mock, + authInteractor: AuthInteractor.mock, router: CourseRouterMock(), config: ConfigMock(), connectivity: Connectivity(), diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 4ad87108e..c13b68edc 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -17,7 +17,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { @Published private(set) var isShowProgress = false @Published var showError: Bool = false @Published var downloadState: [String: DownloadViewState] = [:] - @Published var returnCourseSequential: CourseSequential? + @Published var continueWith: ContinueWith? var errorMessage: String? { didSet { @@ -27,29 +27,33 @@ public class CourseContainerViewModel: BaseCourseViewModel { } } - public let interactor: CourseInteractorProtocol - public let router: CourseRouter - public let config: Config - public let connectivity: ConnectivityProtocol + private let interactor: CourseInteractorProtocol + private let authInteractor: AuthInteractorProtocol + let router: CourseRouter + let config: Config + let connectivity: ConnectivityProtocol - public let isActive: Bool? - public let courseStart: Date? - public let courseEnd: Date? - public let enrollmentStart: Date? - public let enrollmentEnd: Date? + let isActive: Bool? + let courseStart: Date? + let courseEnd: Date? + let enrollmentStart: Date? + let enrollmentEnd: Date? - public init(interactor: CourseInteractorProtocol, - router: CourseRouter, - config: Config, - connectivity: ConnectivityProtocol, - manager: DownloadManagerProtocol, - isActive: Bool?, - courseStart: Date?, - courseEnd: Date?, - enrollmentStart: Date?, - enrollmentEnd: Date? + public init( + interactor: CourseInteractorProtocol, + authInteractor: AuthInteractorProtocol, + router: CourseRouter, + config: Config, + connectivity: ConnectivityProtocol, + manager: DownloadManagerProtocol, + isActive: Bool?, + courseStart: Date?, + courseEnd: Date?, + enrollmentStart: Date?, + enrollmentEnd: Date? ) { self.interactor = interactor + self.authInteractor = authInteractor self.router = router self.config = config self.connectivity = connectivity @@ -72,7 +76,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { } @MainActor - public func getCourseBlocks(courseID: String, withProgress: Bool = true) async { + func getCourseBlocks(courseID: String, withProgress: Bool = true) async { if let courseStart { if courseStart < Date() { isShowProgress = withProgress @@ -81,10 +85,10 @@ public class CourseContainerViewModel: BaseCourseViewModel { courseStructure = try await interactor.getCourseBlocks(courseID: courseID) isShowProgress = false if let courseStructure { - let returnCourseSequential = try await getResumeBlock(courseID: courseID, - courseStructure: courseStructure) + let continueWith = try await getResumeBlock(courseID: courseID, + courseStructure: courseStructure) withAnimation { - self.returnCourseSequential = returnCourseSequential + self.continueWith = continueWith } } } else { @@ -107,10 +111,17 @@ public class CourseContainerViewModel: BaseCourseViewModel { } @MainActor - private func getResumeBlock(courseID: String, courseStructure: CourseStructure) async throws -> CourseSequential? { + func tryToRefreshCookies() async { + try? await authInteractor.getCookies(force: false) + } + + @MainActor + private func getResumeBlock(courseID: String, courseStructure: CourseStructure) async throws -> ContinueWith? { let result = try await interactor.resumeBlock(courseID: courseID) - return findCourseSequential(blockID: result.blockID, - courseStructure: courseStructure) + return findContinueVertical( + blockID: result.blockID, + courseStructure: courseStructure + ) } func onDownloadViewTap(chapter: CourseChapter, blockId: String, state: DownloadViewState) { @@ -174,14 +185,19 @@ public class CourseContainerViewModel: BaseCourseViewModel { } } - private func findCourseSequential(blockID: String, courseStructure: CourseStructure) -> CourseSequential? { - for chapter in courseStructure.childs { - for sequential in chapter.childs { - for vertical in sequential.childs { - for block in vertical.childs { - if block.id == blockID { - return sequential - } + private func findContinueVertical(blockID: String, courseStructure: CourseStructure) -> ContinueWith? { + for chapterIndex in courseStructure.childs.indices { + let chapter = courseStructure.childs[chapterIndex] + for sequentialIndex in chapter.childs.indices { + let sequential = chapter.childs[sequentialIndex] + for verticalIndex in sequential.childs.indices { + let vertical = sequential.childs[verticalIndex] + for block in vertical.childs where block.id == blockID { + return ContinueWith( + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex, + verticalIndex: verticalIndex + ) } } } diff --git a/Course/Course/Presentation/CourseRouter.swift b/Course/Course/Presentation/CourseRouter.swift index 83e1181ba..58f5944c6 100644 --- a/Course/Course/Presentation/CourseRouter.swift +++ b/Course/Course/Presentation/CourseRouter.swift @@ -10,32 +10,52 @@ import Core public protocol CourseRouter: BaseRouter { - func showCourseScreens(courseID: String, - isActive: Bool?, - courseStart: Date?, - courseEnd: Date?, - enrollmentStart: Date?, - enrollmentEnd: Date?, - title: String) + func showCourseScreens( + courseID: String, + isActive: Bool?, + courseStart: Date?, + courseEnd: Date?, + enrollmentStart: Date?, + enrollmentEnd: Date?, + title: String + ) - func showCourseUnit(blockId: String, - courseID: String, - sectionName: String, - blocks: [CourseBlock]) + func showCourseUnit( + id: String, + blockId: String, + courseID: String, + sectionName: String, + verticalIndex: Int, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) - func showCourseVerticalView(title: String, - verticals: [CourseVertical]) + func replaceCourseUnit( + id: String, + blockId: String, + courseID: String, + sectionName: String, + verticalIndex: Int, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) - func showCourseBlocksView(title: String, - blocks: [CourseBlock]) + func showCourseVerticalView( + id: String, + title: String, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) - func showCourseVerticalAndBlocksView(verticals: (String, [CourseVertical]), - blocks: (String, [CourseBlock])) - - func showHandoutsUpdatesView(handouts: String?, - announcements: [CourseUpdate]?, - router: Course.CourseRouter, - cssInjector: CSSInjector) + func showHandoutsUpdatesView( + handouts: String?, + announcements: [CourseUpdate]?, + router: Course.CourseRouter, + cssInjector: CSSInjector + ) } // Mark - For testing and SwiftUI preview @@ -44,32 +64,52 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { public override init() {} - public func showCourseScreens(courseID: String, - isActive: Bool?, - courseStart: Date?, - courseEnd: Date?, - enrollmentStart: Date?, - enrollmentEnd: Date?, - title: String) {} - - public func showCourseUnit(blockId: String, - courseID: String, - sectionName: String, - blocks: [CourseBlock]) {} + public func showCourseScreens( + courseID: String, + isActive: Bool?, + courseStart: Date?, + courseEnd: Date?, + enrollmentStart: Date?, + enrollmentEnd: Date?, + title: String + ) {} - public func showCourseVerticalView(title: String, - verticals: [CourseVertical]) {} + public func showCourseUnit( + id: String, + blockId: String, + courseID: String, + sectionName: String, + verticalIndex: Int, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) {} - public func showCourseBlocksView(title: String, - blocks: [CourseBlock]) {} + public func replaceCourseUnit( + id: String, + blockId: String, + courseID: String, + sectionName: String, + verticalIndex: Int, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) {} - public func showCourseVerticalAndBlocksView(verticals: (String, [CourseVertical]), - blocks: (String, [CourseBlock])) {} + public func showCourseVerticalView( + id: String, + title: String, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) {} - public func showHandoutsUpdatesView(handouts: String?, - announcements: [CourseUpdate]?, - router: Course.CourseRouter, - cssInjector: CSSInjector) {} + public func showHandoutsUpdatesView( + handouts: String?, + announcements: [CourseUpdate]?, + router: Course.CourseRouter, + cssInjector: CSSInjector + ) {} } #endif diff --git a/Course/Course/Presentation/Details/CourseDetailsViewModel.swift b/Course/Course/Presentation/Details/CourseDetailsViewModel.swift index 6f3e7c8d2..d0b0d4216 100644 --- a/Course/Course/Presentation/Details/CourseDetailsViewModel.swift +++ b/Course/Course/Presentation/Details/CourseDetailsViewModel.swift @@ -33,13 +33,15 @@ public class CourseDetailsViewModel: ObservableObject { let router: CourseRouter let config: Config let cssInjector: CSSInjector - public let connectivity: ConnectivityProtocol + let connectivity: ConnectivityProtocol - public init(interactor: CourseInteractorProtocol, - router: CourseRouter, - config: Config, - cssInjector: CSSInjector, - connectivity: ConnectivityProtocol) { + public init( + interactor: CourseInteractorProtocol, + router: CourseRouter, + config: Config, + cssInjector: CSSInjector, + connectivity: ConnectivityProtocol + ) { self.interactor = interactor self.router = router self.config = config @@ -56,7 +58,7 @@ public class CourseDetailsViewModel: ObservableObject { if let isEnrolled = courseDetails?.isEnrolled { self.courseDetails?.isEnrolled = isEnrolled } - + isShowProgress = false } else { courseDetails = try await interactor.getCourseDetailsOffline(courseID: courseID) @@ -98,7 +100,7 @@ public class CourseDetailsViewModel: ObservableObject { guard let url = URL(string: httpsURL) else { return } UIApplication.shared.open(url) } - + @MainActor func enrollToCourse(id: String) async { do { diff --git a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift index 179e6b55f..598018893 100644 --- a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift @@ -20,7 +20,12 @@ public struct HandoutsUpdatesDetailView: View { private let title: String @State private var height: [Int: CGFloat] = [:] - public init(handouts: String?, announcements: [CourseUpdate]?, router: CourseRouter, cssInjector: CSSInjector) { + public init( + handouts: String?, + announcements: [CourseUpdate]?, + router: CourseRouter, + cssInjector: CSSInjector + ) { if handouts != nil { self.title = CourseLocalization.HandoutsCellHandouts.title } else { @@ -66,19 +71,23 @@ public struct HandoutsUpdatesDetailView: View { GeometryReader { reader in // MARK: - Page name VStack(alignment: .center) { - NavigationBar(title: title, - leftButtonAction: { router.back() }) + NavigationBar( + title: title, + leftButtonAction: { router.back() } + ) // MARK: - Page Body VStack(alignment: .leading) { // MARK: - Handouts if let handouts { - let formattedHandouts = cssInjector.injectCSS(colorScheme: colorScheme, - html: handouts, - type: .discovery, - fontSize: idiom == .pad ? 100 : 300, - screenWidth: .infinity) + let formattedHandouts = cssInjector.injectCSS( + colorScheme: colorScheme, + html: handouts, + type: .discovery, + fontSize: idiom == .pad ? 100 : 300, + screenWidth: .infinity + ) WebViewHtml(fixBrokenLinks(in: formattedHandouts)) } else if let announcements { @@ -89,15 +98,19 @@ public struct HandoutsUpdatesDetailView: View { Text(ann.date) .font(Theme.Fonts.labelSmall) - let formattedAnnouncements = cssInjector.injectCSS(colorScheme: colorScheme, - html: ann.content, - type: .discovery, - screenWidth: reader.size.width) - HTMLFormattedText(fixBrokenLinks(in: formattedAnnouncements), - isScrollEnabled: true, - textViewHeight: $height[index]) + let formattedAnnouncements = cssInjector.injectCSS( + colorScheme: colorScheme, + html: ann.content, + type: .discovery, + screenWidth: reader.size.width + ) + HTMLFormattedText( + fixBrokenLinks(in: formattedAnnouncements), + isScrollEnabled: true, + textViewHeight: $height[index] + ) .frame(height: height[index]) - + if index != announcements.count - 1 { Divider() } @@ -135,13 +148,23 @@ Hi! Welcome to the demonstration course. We built this to help you become more f Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollitia animi, id est laborum. """ - HandoutsUpdatesDetailView(handouts: nil, - announcements: [CourseUpdate(id: 1, date: "1 march", - content: handouts, status: "done"), - CourseUpdate(id: 2, date: "3 april", - content: loremIpsumHtml, status: "nice")], - router: CourseRouterMock(), - cssInjector: CSSInjectorMock()) + HandoutsUpdatesDetailView( + handouts: nil, + announcements: [ + CourseUpdate( + id: 1, + date: "1 march", + content: handouts, + status: "done" + ), + CourseUpdate( + id: 2, + date: "3 april", + content: loremIpsumHtml, + status: "nice")], + router: CourseRouterMock(), + cssInjector: CSSInjectorMock() + ) } } #endif diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index e876b9dd9..bbc0752e9 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -12,10 +12,13 @@ struct HandoutsView: View { private let courseID: String - @ObservedObject private var viewModel: HandoutsViewModel + @ObservedObject + private var viewModel: HandoutsViewModel - public init(courseID: String, - viewModel: HandoutsViewModel) { + public init( + courseID: String, + viewModel: HandoutsViewModel + ) { self.courseID = courseID self.viewModel = viewModel } @@ -60,13 +63,15 @@ struct HandoutsView: View { } // MARK: - Offline mode SnackBar - OfflineSnackBarView(connectivity: viewModel.connectivity, - reloadAction: { - Task { - await viewModel.getHandouts(courseID: courseID) - await viewModel.getUpdates(courseID: courseID) + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + Task { + await viewModel.getHandouts(courseID: courseID) + await viewModel.getUpdates(courseID: courseID) + } } - }) + ) // MARK: - Error Alert if viewModel.showError { diff --git a/Course/Course/Presentation/Handouts/HandoutsViewModel.swift b/Course/Course/Presentation/Handouts/HandoutsViewModel.swift index 2759406cb..2055e4adb 100644 --- a/Course/Course/Presentation/Handouts/HandoutsViewModel.swift +++ b/Course/Course/Presentation/Handouts/HandoutsViewModel.swift @@ -25,15 +25,17 @@ public class HandoutsViewModel: ObservableObject { } private let interactor: CourseInteractorProtocol - public let cssInjector: CSSInjector - public let router: CourseRouter - public let connectivity: ConnectivityProtocol + let cssInjector: CSSInjector + let router: CourseRouter + let connectivity: ConnectivityProtocol - public init(interactor: CourseInteractorProtocol, - router: CourseRouter, - cssInjector: CSSInjector, - connectivity: ConnectivityProtocol, - courseID: String) { + public init( + interactor: CourseInteractorProtocol, + router: CourseRouter, + cssInjector: CSSInjector, + connectivity: ConnectivityProtocol, + courseID: String + ) { self.interactor = interactor self.router = router self.cssInjector = cssInjector diff --git a/Course/Course/Presentation/Outline/ContinueWithView.swift b/Course/Course/Presentation/Outline/ContinueWithView.swift new file mode 100644 index 000000000..9498ec471 --- /dev/null +++ b/Course/Course/Presentation/Outline/ContinueWithView.swift @@ -0,0 +1,137 @@ +// +// ContinueWithView.swift +// Course +// +// Created by  Stepanok Ivan on 29.05.2023. +// + +import SwiftUI +import Core + +struct ContinueWith { + let chapterIndex: Int + let sequentialIndex: Int + let verticalIndex: Int +} + +struct ContinueWithView: View { + let data: ContinueWith + let courseStructure: CourseStructure + let router: CourseRouter + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + init(data: ContinueWith, courseStructure: CourseStructure, router: CourseRouter) { + self.data = data + self.courseStructure = courseStructure + self.router = router + } + + var body: some View { + VStack(alignment: .leading) { + let chapter = courseStructure.childs[data.chapterIndex] + if let vertical = chapter.childs[data.sequentialIndex].childs.first { + if idiom == .pad { + HStack(alignment: .top) { + VStack(alignment: .leading) { + ContinueTitle(vertical: vertical) + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) + Spacer() + UnitButtonView(type: .continueLesson, action: { + router.showCourseVerticalView(id: courseStructure.id, + title: chapter.childs[data.sequentialIndex].displayName, + chapters: courseStructure.childs, + chapterIndex: data.chapterIndex, + sequentialIndex: data.sequentialIndex) + }).frame(width: 200) + } .padding(.horizontal, 24) + .padding(.top, 32) + } else { + VStack(alignment: .leading) { + ContinueTitle(vertical: vertical) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) + } + UnitButtonView(type: .continueLesson, action: { + router.showCourseVerticalView(id: courseStructure.id, + title: chapter.childs[data.sequentialIndex].displayName, + chapters: courseStructure.childs, + chapterIndex: data.chapterIndex, + sequentialIndex: data.sequentialIndex) + }) + } + + } + } .padding(.horizontal, 24) + .padding(.top, 32) + } +} + +private struct ContinueTitle: View { + + let vertical: CourseVertical + + var body: some View { + Text(CoreLocalization.Courseware.continueWith) + .font(Theme.Fonts.labelMedium) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) + HStack { + vertical.type.image + Text(vertical.displayName) + .multilineTextAlignment(.leading) + .font(Theme.Fonts.titleMedium) + .multilineTextAlignment(.leading) + } + } + +} + +#if DEBUG +struct ContinueWithView_Previews: PreviewProvider { + static var previews: some View { + + let childs = [ + CourseChapter( + blockId: "123", + id: "123", + displayName: "Continue lesson", + type: .chapter, + childs: [ + CourseSequential( + blockId: "1", + id: "1", + displayName: "Name", + type: .sequential, + completion: 0, + childs: [ + CourseVertical( + blockId: "1", + id: "1", + displayName: "Vertical", + type: .vertical, + completion: 0, + childs: [ + CourseBlock( + blockId: "2", id: "2", + graded: true, + completion: 0, + type: .html, + displayName: "Continue lesson", + studentUrl: "") + ])])]) + ] + + ContinueWithView(data: ContinueWith(chapterIndex: 0, sequentialIndex: 0, verticalIndex: 0), + courseStructure: CourseStructure(id: "123", + graded: true, + completion: 0, + viewYouTubeUrl: "", + encodedVideo: "", + displayName: "Namaste", + childs: childs, + media: DataLayer.CourseMedia.init(image: + .init(raw: "", small: "", large: "")), + certificate: nil), + router: CourseRouterMock()) + } +} +#endif diff --git a/Course/Course/Presentation/Outline/CourseBlocksView.swift b/Course/Course/Presentation/Outline/CourseBlocksView.swift deleted file mode 100644 index bf44cd0bb..000000000 --- a/Course/Course/Presentation/Outline/CourseBlocksView.swift +++ /dev/null @@ -1,217 +0,0 @@ -// -// CourseBlocksView.swift -// Course -// -// Created by  Stepanok Ivan on 04.02.2023. -// - -import SwiftUI - -import Core -import Kingfisher - -public struct CourseBlocksView: View { - - private var title: String - @ObservedObject - private var viewModel: CourseBlocksViewModel - private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - - public init(title: String, - viewModel: CourseBlocksViewModel) { - self.title = title - self.viewModel = viewModel - } - - public var body: some View { - ZStack(alignment: .top) { - - // MARK: - Page name - GeometryReader { proxy in - VStack(alignment: .center) { - NavigationBar(title: title, - leftButtonAction: { viewModel.router.back() }) - - // MARK: - Page Body - ScrollView { - VStack(alignment: .leading) { - // MARK: - Lessons list - ForEach(viewModel.blocks, id: \.id) { block in - let index = viewModel.blocks.firstIndex(where: { $0.id == block.id }) - Button(action: { - viewModel.router.showCourseUnit(blockId: block.id, - courseID: block.blockId, - sectionName: title, - blocks: viewModel.blocks) - }, label: { - HStack { - Group { - if block.completion == 1 { - CoreAssets.finished.swiftUIImage - .renderingMode(.template) - .foregroundColor(.accentColor) - } else { - block.type.image - } - Text(block.displayName) - .multilineTextAlignment(.leading) - .font(Theme.Fonts.titleMedium) - .lineLimit(1) - .frame(maxWidth: idiom == .pad - ? proxy.size.width * 0.5 - : proxy.size.width * 0.6, - alignment: .leading) - }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) - Spacer() - if let state = viewModel.downloadState[block.id] { - switch state { - case .available: - DownloadAvailableView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: block.id, state: state) - } - .onForeground { - viewModel.onForeground() - } - case .downloading: - DownloadProgressView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: block.id, state: state) - } - .onBackground { - viewModel.onBackground() - } - case .finished: - DownloadFinishedView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: block.id, state: state) - } - } - } - Image(systemName: "chevron.right") - .padding(.vertical, 8) - } - }).padding(.horizontal, 36) - .padding(.vertical, 14) - if index != viewModel.blocks.count - 1 { - Divider() - .frame(height: 1) - .overlay(CoreAssets.cardViewStroke.swiftUIColor) - .padding(.horizontal, 24) - } - } - } - Spacer(minLength: 84) - }.frameLimit() - .onRightSwipeGesture { - viewModel.router.back() - } - } - } - - // MARK: - Offline mode SnackBar - OfflineSnackBarView(connectivity: viewModel.connectivity, - reloadAction: { }) - - // MARK: - Error Alert - if viewModel.showError { - VStack { - Spacer() - SnackBarView(message: viewModel.errorMessage) - } - .padding(.bottom, viewModel.connectivity.isInternetAvaliable - ? 0 : OfflineSnackBarView.height) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil - } - } - } - - } - .background( - CoreAssets.background.swiftUIColor - .ignoresSafeArea() - ) - } -} - -#if DEBUG -struct CourseBlocksView_Previews: PreviewProvider { - static var previews: some View { - let blocks: [CourseBlock] = [ - CourseBlock( - blockId: "block_1", - id: "1", - topicId: nil, - graded: true, - completion: 0, - type: .html, - displayName: "HTML Block", - studentUrl: "", - videoUrl: nil, - youTubeUrl: nil - ), - CourseBlock( - blockId: "block_2", - id: "2", - topicId: nil, - graded: true, - completion: 0, - type: .problem, - displayName: "Problem Block", - studentUrl: "", - videoUrl: nil, - youTubeUrl: nil - ), - CourseBlock( - blockId: "block_3", - id: "3", - topicId: nil, - graded: true, - completion: 1, - type: .problem, - displayName: "Completed Problem Block", - studentUrl: "", - videoUrl: nil, - youTubeUrl: nil - ), - CourseBlock( - blockId: "block_4", - id: "4", - topicId: nil, - graded: true, - completion: 0, - type: .video, - displayName: "Video Block", - studentUrl: "", - videoUrl: "some_data", - youTubeUrl: nil - ) - ] - - let viewModel = CourseBlocksViewModel(blocks: blocks, - manager: DownloadManagerMock(), - router: CourseRouterMock(), - connectivity: Connectivity()) - - return Group { - CourseBlocksView( - title: "Course title", - viewModel: viewModel - ) - .preferredColorScheme(.light) - .previewDisplayName("CourseBlocksView Light") - - CourseBlocksView( - title: "Course title", - viewModel: viewModel - ) - .preferredColorScheme(.dark) - .previewDisplayName("CourseBlocksView Dark") - } - - } -} -#endif diff --git a/Course/Course/Presentation/Outline/CourseBlocksViewModel.swift b/Course/Course/Presentation/Outline/CourseBlocksViewModel.swift deleted file mode 100644 index 9dba81c5a..000000000 --- a/Course/Course/Presentation/Outline/CourseBlocksViewModel.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// CourseBlocksViewModel.swift -// Course -// -// Created by  Stepanok Ivan on 14.03.2023. -// - -import SwiftUI -import Core -import Combine - -public class CourseBlocksViewModel: BaseCourseViewModel { - let router: CourseRouter - let connectivity: ConnectivityProtocol - @Published var blocks: [CourseBlock] - @Published var downloadState: [String: DownloadViewState] = [:] - @Published var showError: Bool = false - - var errorMessage: String? { - didSet { - withAnimation { - showError = errorMessage != nil - } - } - } - - public init(blocks: [CourseBlock], - manager: DownloadManagerProtocol, - router: CourseRouter, - connectivity: ConnectivityProtocol) { - self.blocks = blocks - self.router = router - self.connectivity = connectivity - - super.init(manager: manager) - - manager.publisher() - .sink(receiveValue: { [weak self] _ in - guard let self else { return } - DispatchQueue.main.async { - self.setDownloadsStates() - } - }) - .store(in: &cancellables) - - setDownloadsStates() - } - - func onDownloadViewTap(blockId: String, state: DownloadViewState) { - if let block = blocks.first(where: { $0.id == blockId }) { - do { - switch state { - case .available: - try manager.addToDownloadQueue(blocks: [block]) - downloadState[block.id] = .downloading - case .downloading: - try manager.cancelDownloading(blocks: [block]) - downloadState[block.id] = .available - case .finished: - manager.deleteFile(blocks: [block]) - downloadState[block.id] = .available - } - } catch let error { - if error is NoWiFiError { - errorMessage = CoreLocalization.Error.wifi - } - } - } - } - - private func setDownloadsStates() { - let downloads = manager.getAllDownloads() - var states: [String: DownloadViewState] = [:] - for block in blocks where block.isDownloadable { - if let download = downloads.first(where: { $0.id == block.id }) { - switch download.state { - case .waiting, .inProgress: - states[download.id] = .downloading - case .paused: - states[download.id] = .available - case .finished: - states[download.id] = .finished - } - } else { - states[block.id] = .available - } - } - downloadState = states - } -} diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 8cc925146..4e3b309a6 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -38,7 +38,7 @@ public struct CourseOutlineView: View { GeometryReader { proxy in VStack(alignment: .center) { NavigationBar(title: title, - leftButtonAction: { viewModel.router.back() }) + leftButtonAction: { viewModel.router.back() }) // MARK: - Page Body RefreshableScrollViewCompat(action: { @@ -90,17 +90,24 @@ public struct CourseOutlineView: View { .fixedSize(horizontal: false, vertical: true) if !isVideo { - if let sequential = viewModel.returnCourseSequential { - ContinueWithView(sequential: sequential, viewModel: viewModel) + if let continueWith = viewModel.continueWith, + let courseStructure = viewModel.courseStructure { + ContinueWithView( + data: continueWith, + courseStructure: courseStructure, + router: viewModel.router + ) } } - if let courseStructure = isVideo ? viewModel.courseVideosStructure : viewModel.courseStructure { + if let courseStructure = isVideo + ? viewModel.courseVideosStructure + : viewModel.courseStructure { // MARK: - Sections list let chapters = courseStructure.childs ForEach(chapters, id: \.id) { chapter in - let index = chapters.firstIndex(where: {$0.id == chapter.id }) + let chapterIndex = chapters.firstIndex(where: { $0.id == chapter.id }) Text(chapter.displayName) .font(Theme.Fonts.titleMedium) .multilineTextAlignment(.leading) @@ -108,10 +115,18 @@ public struct CourseOutlineView: View { .padding(.horizontal, 24) .padding(.top, 40) ForEach(chapter.childs, id: \.id) { child in + let sequentialIndex = chapter.childs.firstIndex(where: { $0.id == child.id }) VStack(alignment: .leading) { Button(action: { - viewModel.router.showCourseVerticalView(title: child.displayName, - verticals: child.childs) + if let chapterIndex, let sequentialIndex { + viewModel.router.showCourseVerticalView( + id: courseID, + title: child.displayName, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + } }, label: { Group { child.type.image @@ -119,10 +134,12 @@ public struct CourseOutlineView: View { .font(Theme.Fonts.titleMedium) .multilineTextAlignment(.leading) .lineLimit(1) - .frame(maxWidth: idiom == .pad - ? proxy.size.width * 0.5 - : proxy.size.width * 0.6, - alignment: .leading) + .frame( + maxWidth: idiom == .pad + ? proxy.size.width * 0.5 + : proxy.size.width * 0.6, + alignment: .leading + ) }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() if let state = viewModel.downloadState[child.id] { @@ -166,7 +183,7 @@ public struct CourseOutlineView: View { .foregroundColor(CoreAssets.accentColor.swiftUIColor) }).padding(.horizontal, 36) .padding(.vertical, 20) - if index != chapters.count - 1 { + if chapterIndex != chapters.count - 1 { Divider() .frame(height: 1) .overlay(CoreAssets.cardViewStroke.swiftUIColor) @@ -191,10 +208,12 @@ public struct CourseOutlineView: View { } // MARK: - Offline mode SnackBar - OfflineSnackBarView(connectivity: viewModel.connectivity, - reloadAction: { - await viewModel.getCourseBlocks(courseID: courseID, withProgress: isIOS14) - }) + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.getCourseBlocks(courseID: courseID, withProgress: isIOS14) + } + ) // MARK: - Error Alert if viewModel.showError { @@ -228,73 +247,20 @@ public struct CourseOutlineView: View { } } -struct ContinueWithView: View { - let sequential: CourseSequential - let viewModel: CourseContainerViewModel - private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - - var body: some View { - if idiom == .pad { - HStack(alignment: .top) { - if let vertical = sequential.childs.first { - VStack(alignment: .leading) { - Text(CourseLocalization.Courseware.continueWith) - .font(Theme.Fonts.labelMedium) - .foregroundColor(CoreAssets.textSecondary.swiftUIColor) - HStack { - vertical.type.image - Text(vertical.displayName) - .multilineTextAlignment(.leading) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - } - }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) - Spacer() - UnitButtonView(type: .continueLesson, action: { - viewModel.router.showCourseVerticalAndBlocksView(verticals: (sequential.displayName, sequential.childs), - blocks: (vertical.displayName, vertical.childs)) - }).frame(width: 200) - } - } .padding(.horizontal, 24) - .padding(.top, 32) - } else { - VStack(alignment: .leading) { - if let vertical = sequential.childs.first { - Text(CourseLocalization.Courseware.continueWith) - .font(Theme.Fonts.labelMedium) - .foregroundColor(CoreAssets.textSecondary.swiftUIColor) - HStack { - vertical.type.image - Text(vertical.displayName) - .multilineTextAlignment(.leading) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) - UnitButtonView(type: .continueLesson, action: { - viewModel.router.showCourseVerticalAndBlocksView(verticals: (sequential.displayName, sequential.childs), - blocks: (vertical.displayName, vertical.childs)) - }) - } - } - .padding(.horizontal, 24) - .padding(.top, 32) - } - } -} - #if DEBUG struct CourseOutlineView_Previews: PreviewProvider { static var previews: some View { let viewModel = CourseContainerViewModel( interactor: CourseInteractor.mock, + authInteractor: AuthInteractor.mock, router: CourseRouterMock(), config: ConfigMock(), connectivity: Connectivity(), manager: DownloadManagerMock(), - isActive: nil, + isActive: true, courseStart: Date(), courseEnd: nil, - enrollmentStart: nil, + enrollmentStart: Date(), enrollmentEnd: nil ) Task { diff --git a/Course/Course/Presentation/Outline/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVerticalView.swift index f68e7b314..653f78e76 100644 --- a/Course/Course/Presentation/Outline/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVerticalView.swift @@ -13,15 +13,18 @@ import Kingfisher public struct CourseVerticalView: View { private var title: String + private let id: String @ObservedObject private var viewModel: CourseVerticalViewModel private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } public init( title: String, + id: String, viewModel: CourseVerticalViewModel ) { self.title = title + self.id = id self.viewModel = viewModel } @@ -29,7 +32,7 @@ public struct CourseVerticalView: View { ZStack(alignment: .top) { VStack(alignment: .center) { NavigationBar(title: title, - leftButtonAction: { viewModel.router.back() }) + leftButtonAction: { viewModel.router.back() }) // MARK: - Page Body GeometryReader { proxy in @@ -37,68 +40,84 @@ public struct CourseVerticalView: View { VStack(alignment: .leading) { // MARK: - Lessons list ForEach(viewModel.verticals, id: \.id) { vertical in - let index = viewModel.verticals.firstIndex(where: {$0.id == vertical.id}) - Button(action: { - viewModel.router.showCourseBlocksView( - title: vertical.displayName, - blocks: vertical.childs - ) - }, label: { - HStack { - Group { - if vertical.completion == 1 { - CoreAssets.finished.swiftUIImage - .renderingMode(.template) - .foregroundColor(.accentColor) - } else { - vertical.type.image - } - Text(vertical.displayName) - .font(Theme.Fonts.titleMedium) - .lineLimit(1) - .frame(maxWidth: idiom == .pad - ? proxy.size.width * 0.5 - : proxy.size.width * 0.6, - alignment: .leading) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) - Spacer() - if let state = viewModel.downloadState[vertical.id] { - switch state { - case .available: - DownloadAvailableView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: vertical.id, state: state) - } - .onForeground { - viewModel.onForeground() - } - case .downloading: - DownloadProgressView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: vertical.id, state: state) - } - .onBackground { - viewModel.onBackground() - } - case .finished: - DownloadFinishedView() - .onTapGesture { - viewModel.onDownloadViewTap(blockId: vertical.id, state: state) - } + if let index = viewModel.verticals.firstIndex(where: {$0.id == vertical.id}) { + Button(action: { + if let block = viewModel.verticals[index].childs.first { + viewModel.router.showCourseUnit(id: id, + blockId: block.id, + courseID: block.blockId, + sectionName: block.displayName, + verticalIndex: index, + chapters: viewModel.chapters, + chapterIndex: viewModel.chapterIndex, + sequentialIndex: viewModel.sequentialIndex) + } + }, label: { + HStack { + Group { + if vertical.completion == 1 { + CoreAssets.finished.swiftUIImage + .renderingMode(.template) + .foregroundColor(.accentColor) + } else { + vertical.type.image + } + Text(vertical.displayName) + .font(Theme.Fonts.titleMedium) + .lineLimit(1) + .frame(maxWidth: idiom == .pad + ? proxy.size.width * 0.5 + : proxy.size.width * 0.6, + alignment: .leading) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) + Spacer() + if let state = viewModel.downloadState[vertical.id] { + switch state { + case .available: + DownloadAvailableView() + .onTapGesture { + viewModel.onDownloadViewTap( + blockId: vertical.id, + state: state + ) + } + .onForeground { + viewModel.onForeground() + } + case .downloading: + DownloadProgressView() + .onTapGesture { + viewModel.onDownloadViewTap( + blockId: vertical.id, + state: state + ) + } + .onBackground { + viewModel.onBackground() + } + case .finished: + DownloadFinishedView() + .onTapGesture { + viewModel.onDownloadViewTap( + blockId: vertical.id, + state: state + ) + } + } } + Image(systemName: "chevron.right") + .padding(.vertical, 8) } - Image(systemName: "chevron.right") - .padding(.vertical, 8) + }).padding(.horizontal, 36) + .padding(.vertical, 14) + if index != viewModel.verticals.count - 1 { + Divider() + .frame(height: 1) + .overlay(CoreAssets.cardViewStroke.swiftUIColor) + .padding(.horizontal, 24) } - }).padding(.horizontal, 36) - .padding(.vertical, 14) - if index != viewModel.verticals.count - 1 { - Divider() - .frame(height: 1) - .overlay(CoreAssets.cardViewStroke.swiftUIColor) - .padding(.horizontal, 24) } } } @@ -112,7 +131,7 @@ public struct CourseVerticalView: View { // MARK: - Offline mode SnackBar OfflineSnackBarView(connectivity: viewModel.connectivity, - reloadAction: { }) + reloadAction: { }) // MARK: - Error Alert if viewModel.showError { @@ -140,47 +159,48 @@ public struct CourseVerticalView: View { #if DEBUG struct CourseVerticalView_Previews: PreviewProvider { static var previews: some View { - - let verticals: [CourseVertical] = [ - CourseVertical( - blockId: "block_1", + let chapters = [ + CourseChapter( + blockId: "1", id: "1", - displayName: "Some vertical", - type: .vertical, - completion: 0, - childs: [] - ), - CourseVertical( - blockId: "block_2", - id: "2", - displayName: "Comleted vertical", - type: .vertical, - completion: 1, - childs: [] - ), - CourseVertical( - blockId: "block_3", - id: "3", - displayName: "Another vertical", - type: .vertical, - completion: 0, - childs: [] - ) + displayName: "Chapter 1", + type: .chapter, + childs: [ + CourseSequential( + blockId: "3", + id: "3", + displayName: "Sequential", + type: .sequential, + completion: 1, + childs: [ + CourseVertical( + blockId: "4", + id: "4", + displayName: "Vertical", + type: .vertical, + completion: 0, + childs: []) + ]) + ]) ] - let viewModel = CourseVerticalViewModel(verticals: verticals, - manager: DownloadManagerMock(), - router: CourseRouterMock(), - connectivity: Connectivity()) + let viewModel = CourseVerticalViewModel( + chapters: chapters, + chapterIndex: 0, + sequentialIndex: 0, + manager: DownloadManagerMock(), + router: CourseRouterMock(), + connectivity: Connectivity() + ) return Group { - CourseVerticalView(title: "Course title", viewModel: viewModel) - .preferredColorScheme(.light) - .previewDisplayName("CourseVerticalView Light") + CourseVerticalView(title: "Course title", id: "1", viewModel: viewModel) + .preferredColorScheme(.light) + .previewDisplayName("CourseVerticalView Light") - CourseVerticalView(title: "Course title", viewModel: viewModel) - .preferredColorScheme(.dark) - .previewDisplayName("CourseVerticalView Dark") + CourseVerticalView(title: "Course title", id: "1", viewModel: viewModel) + .preferredColorScheme(.dark) + .previewDisplayName("CourseVerticalView Dark") } } diff --git a/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift b/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift index 54fc1ca01..9d0bd77c5 100644 --- a/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift +++ b/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift @@ -15,7 +15,10 @@ public class CourseVerticalViewModel: BaseCourseViewModel { @Published var verticals: [CourseVertical] @Published var downloadState: [String: DownloadViewState] = [:] @Published var showError: Bool = false - + let chapters: [CourseChapter] + let chapterIndex: Int + let sequentialIndex: Int + var errorMessage: String? { didSet { withAnimation { @@ -24,14 +27,20 @@ public class CourseVerticalViewModel: BaseCourseViewModel { } } - public init(verticals: [CourseVertical], - manager: DownloadManagerProtocol, - router: CourseRouter, - connectivity: ConnectivityProtocol) { - self.verticals = verticals + public init( + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int, + manager: DownloadManagerProtocol, + router: CourseRouter, + connectivity: ConnectivityProtocol + ) { + self.chapters = chapters + self.chapterIndex = chapterIndex + self.sequentialIndex = sequentialIndex self.router = router self.connectivity = connectivity - + self.verticals = chapters[chapterIndex].childs[sequentialIndex].childs super.init(manager: manager) manager.publisher() @@ -68,7 +77,7 @@ public class CourseVerticalViewModel: BaseCourseViewModel { } } } - + private func setDownloadsStates() { let downloads = manager.getAllDownloads() var states: [String: DownloadViewState] = [:] diff --git a/Course/Course/Presentation/Unit/CourseNavigationView.swift b/Course/Course/Presentation/Unit/CourseNavigationView.swift index 9692e14fe..a4b9f9d85 100644 --- a/Course/Course/Presentation/Unit/CourseNavigationView.swift +++ b/Course/Course/Presentation/Unit/CourseNavigationView.swift @@ -7,80 +7,153 @@ import SwiftUI import Core +import Combine struct CourseNavigationView: View { - @ObservedObject private var viewModel: CourseUnitViewModel + @ObservedObject + private var viewModel: CourseUnitViewModel private let sectionName: String - @Binding var killPlayer: Bool - - init(sectionName: String, viewModel: CourseUnitViewModel, killPlayer: Binding) { + private let playerStateSubject: CurrentValueSubject + + init( + sectionName: String, + viewModel: CourseUnitViewModel, + playerStateSubject: CurrentValueSubject + ) { self.viewModel = viewModel self.sectionName = sectionName - self._killPlayer = killPlayer + self.playerStateSubject = playerStateSubject } var body: some View { - HStack(alignment: .top, spacing: 24) { - if viewModel.selectedLesson() == viewModel.blocks.first - && viewModel.blocks.count != 1 { - UnitButtonView(type: .first, action: { - killPlayer.toggle() + HStack(alignment: .top, spacing: 7) { + if viewModel.selectedLesson() == viewModel.verticals[viewModel.verticalIndex].childs.first + && viewModel.verticals[viewModel.verticalIndex].childs.count != 1 { + UnitButtonView(type: .nextBig, action: { + playerStateSubject.send(VideoPlayerState.pause) viewModel.select(move: .next) - viewModel.createLessonType() - }) + }).frame(width: 215) } else { - - if viewModel.previousLesson != "" { - UnitButtonView(type: .previous, action: { - killPlayer.toggle() - viewModel.select(move: .previous) - viewModel.createLessonType() - }) - } - if viewModel.nextLesson != "" { - UnitButtonView(type: .next, action: { - killPlayer.toggle() - viewModel.select(move: .next) - viewModel.createLessonType() - }) - } - if viewModel.selectedLesson() == viewModel.blocks.last { - UnitButtonView(type: viewModel.blocks.count == 1 ? .finish : .last, action: { + if viewModel.selectedLesson() == viewModel.verticals[viewModel.verticalIndex].childs.last { + if viewModel.selectedLesson() != viewModel.verticals[viewModel.verticalIndex].childs.first { + UnitButtonView(type: .previous, action: { + playerStateSubject.send(VideoPlayerState.pause) + viewModel.select(move: .previous) + }) + } + UnitButtonView(type: .last, action: { + let sequentials = viewModel.chapters[viewModel.chapterIndex].childs + let verticals = viewModel + .chapters[viewModel.chapterIndex] + .childs[viewModel.sequentialIndex] + .childs + let chapters = viewModel.chapters + let currentVertical = viewModel.verticals[viewModel.verticalIndex] + viewModel.router.presentAlert( alertTitle: CourseLocalization.Courseware.goodWork, alertMessage: (CourseLocalization.Courseware.section - + " " + sectionName + " " + CourseLocalization.Courseware.isFinished), + + currentVertical.displayName + CourseLocalization.Courseware.isFinished), + nextSectionName: { + if viewModel.verticals.count > viewModel.verticalIndex + 1 { + return viewModel.verticals[viewModel.verticalIndex + 1].displayName + } else if sequentials.count > viewModel.sequentialIndex + 1 { + return sequentials[viewModel.sequentialIndex + 1].childs.first?.displayName + } else if chapters.count > viewModel.chapterIndex + 1 { + return chapters[viewModel.chapterIndex + 1].childs.first?.childs.first?.displayName + } else { + return nil + } + }(), action: CourseLocalization.Courseware.backToOutline, image: CoreAssets.goodWork.swiftUIImage, - onCloseTapped: {}, + onCloseTapped: { viewModel.router.dismiss(animated: false) }, okTapped: { - killPlayer.toggle() + playerStateSubject.send(VideoPlayerState.pause) + playerStateSubject.send(VideoPlayerState.kill) + viewModel.router.dismiss(animated: false) + viewModel.router.back(animated: true) + }, + nextSectionTapped: { + playerStateSubject.send(VideoPlayerState.pause) + playerStateSubject.send(VideoPlayerState.kill) viewModel.router.dismiss(animated: false) - viewModel.router.removeLastView(controllers: 2) + + let chapterIndex: Int + let sequentialIndex: Int + let verticalIndex: Int + + // Switch to the next Vertical + if verticals.count - 1 > viewModel.verticalIndex { + chapterIndex = viewModel.chapterIndex + sequentialIndex = viewModel.sequentialIndex + verticalIndex = viewModel.verticalIndex + 1 + // Switch to the next Sequential + } else if sequentials.count - 1 > viewModel.sequentialIndex { + chapterIndex = viewModel.chapterIndex + sequentialIndex = viewModel.sequentialIndex + 1 + verticalIndex = 0 + } else { + // Switch to the next Chapter + chapterIndex = viewModel.chapterIndex + 1 + sequentialIndex = 0 + verticalIndex = 0 + } + + viewModel.router.replaceCourseUnit( + id: viewModel.id, + blockId: viewModel.lessonID, + courseID: viewModel.courseID, + sectionName: viewModel.selectedLesson().displayName, + verticalIndex: verticalIndex, + chapters: viewModel.chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex) } ) }) + } else { + if viewModel.selectedLesson() != viewModel.verticals[viewModel.verticalIndex].childs.first { + UnitButtonView(type: .previous, action: { + playerStateSubject.send(VideoPlayerState.pause) + viewModel.select(move: .previous) + }) + } + + UnitButtonView(type: .next, action: { + playerStateSubject.send(VideoPlayerState.pause) + viewModel.select(move: .next) + }) } } }.frame(minWidth: 0, maxWidth: .infinity) .padding(.horizontal, 24) - } } #if DEBUG struct CourseNavigationView_Previews: PreviewProvider { static var previews: some View { - let viewModel = CourseUnitViewModel(lessonID: "1", - courseID: "1", - blocks: [], - interactor: CourseInteractor.mock, - router: CourseRouterMock(), - connectivity: Connectivity(), - manager: DownloadManagerMock()) + let viewModel = CourseUnitViewModel( + lessonID: "1", + courseID: "1", + id: "1", + chapters: [], + chapterIndex: 1, + sequentialIndex: 1, + verticalIndex: 1, + interactor: CourseInteractor.mock, + router: CourseRouterMock(), + connectivity: Connectivity(), + manager: DownloadManagerMock() + ) - CourseNavigationView(sectionName: "Name", viewModel: viewModel, killPlayer: .constant(false)) + CourseNavigationView( + sectionName: "Name", + viewModel: viewModel, + playerStateSubject: CurrentValueSubject(nil) + ) } } #endif diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 92af45d6c..56338ea7b 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -10,6 +10,8 @@ import SwiftUI import Core import Discussion import Swinject +import Introspect +import Combine public struct CourseUnitView: View { @@ -22,223 +24,307 @@ public struct CourseUnitView: View { } } } - @State var killPlayer: Bool = false + @State var offsetView: CGFloat = 0 + @State var showDiscussion: Bool = false + private let sectionName: String + public let playerStateSubject = CurrentValueSubject(nil) public init(viewModel: CourseUnitViewModel, sectionName: String) { self.viewModel = viewModel self.sectionName = sectionName viewModel.loadIndex() - viewModel.createLessonType() viewModel.nextTitles() } public var body: some View { ZStack(alignment: .top) { - - // MARK: - Page name - VStack(alignment: .center) { - NavigationBar(title: "", - leftButtonAction: { - viewModel.router.back() - killPlayer.toggle() - }) - - // MARK: - Page Body - VStack { - ZStack(alignment: .top) { - VStack(alignment: .leading) { - if viewModel.connectivity.isInternetAvaliable - || viewModel.lessonType != .video(videoUrl: "", blockID: "") { - switch viewModel.lessonType { - case let .youtube(url, blockID): - VStack(alignment: .leading) { - Text(viewModel.selectedLesson().displayName) - .font(Theme.Fonts.titleLarge) - .padding(.horizontal, 24) - YouTubeVideoPlayer(url: url, - blockID: blockID, - courseID: viewModel.courseID, - languages: viewModel.languages()) - Spacer() - - }.background(CoreAssets.background.swiftUIColor) - case let .video(encodedUrl, blockID): - Text(viewModel.selectedLesson().displayName) - .font(Theme.Fonts.titleLarge) - .padding(.horizontal, 24) - EncodedVideoPlayer( - url: viewModel.urlForVideoFileOrFallback(blockId: blockID, url: encodedUrl), - blockID: blockID, - courseID: viewModel.courseID, - languages: viewModel.languages(), - killPlayer: $killPlayer - ) - Spacer() - case .web(let url): - VStack { - WebUnitView(url: url, viewModel: Container.shared.resolve(WebUnitViewModel.self)!) - }.background(Color.white) - .contrast(1.08) - .padding(.horizontal, -12) - .roundedBackground(strokeColor: .clear, maxIpadWidth: .infinity) - - case .unknown(let url): - Spacer() + // MARK: - Page Body + ZStack(alignment: .bottom) { + GeometryReader { reader in + VStack(spacing: 0) { + if viewModel.connectivity.isInternetAvaliable { + NavigationBar(title: "", + leftButtonAction: { + viewModel.router.back() + playerStateSubject.send(VideoPlayerState.kill) + }).padding(.top, 50) + + LazyVStack(spacing: 0) { + let data = Array(viewModel.verticals[viewModel.verticalIndex].childs.enumerated()) + ForEach(data, id: \.offset) { index, block in VStack(spacing: 0) { - CoreAssets.notAvaliable.swiftUIImage - Text(CourseLocalization.NotAvaliable.title) - .font(Theme.Fonts.titleLarge) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding(.top, 40) - Text(CourseLocalization.NotAvaliable.description) - .font(Theme.Fonts.bodyLarge) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding(.top, 12) - StyledButton(CourseLocalization.NotAvaliable.button, action: { - if let url = URL(string: url) { - UIApplication.shared.open(url) + if index >= viewModel.index - 1 && index <= viewModel.index + 1 { + switch LessonType.from(block) { + // MARK: YouTube + case let .youtube(url, blockID): + YouTubeView( + name: block.displayName, + url: url, + courseID: viewModel.courseID, + blockID: blockID, + playerStateSubject: playerStateSubject, + languages: block.subtitles ?? [], + isOnScreen: index == viewModel.index + ).frameLimit() + Spacer(minLength: 100) + + // MARK: Encoded Video + case let .video(encodedUrl, blockID): + EncodedVideoView( + name: block.displayName, + url: viewModel.urlForVideoFileOrFallback( + blockId: blockID, + url: encodedUrl + ), + courseID: viewModel.courseID, + blockID: blockID, + playerStateSubject: playerStateSubject, + languages: block.subtitles ?? [], + isOnScreen: index == viewModel.index + ).frameLimit() + Spacer(minLength: 100) + // MARK: Web + case .web(let url): + WebView(url: url, viewModel: viewModel) + // MARK: Unknown + case .unknown(let url): + UnknownView(url: url, viewModel: viewModel) + Spacer() + // MARK: Discussion + case let .discussion(blockID, blockKey, title): + VStack { + if showDiscussion { + DiscussionView( + id: viewModel.id, + blockID: blockID, + blockKey: blockKey, + title: title, + viewModel: viewModel + ) + Spacer(minLength: 100) + } else { + DiscussionView( + id: viewModel.id, + blockID: blockID, + blockKey: blockKey, + title: title, + viewModel: viewModel + ).drawingGroup() + Spacer(minLength: 100) + } + }.frameLimit() } - }).frame(width: 215).padding(.top, 40) - }.padding(24) - Spacer() - case let .discussion(blockID): - let id = "course-v1:" - + (viewModel.lessonID.find(from: "block-v1:", to: "+type").first ?? "") - PostsView(courseID: id, - currentBlockID: blockID, - topics: Topics(coursewareTopics: [], - nonCoursewareTopics: []), - title: "", type: .courseTopics(topicID: blockID), - viewModel: Container.shared.resolve(PostsViewModel.self)!, - router: Container.shared.resolve(DiscussionRouter.self)!, - showTopMenu: false) - .onAppear { - Task { - await viewModel.blockCompletionRequest(blockID: blockID) + } else { + EmptyView() } } - default: - VStack {} + .frame(height: reader.size.height) + .id(index) } - } else { - VStack(spacing: 28) { - Image(systemName: "wifi").resizable() - .scaledToFit() - .frame(width: 100) - Text(CourseLocalization.Error.noInternet) - .multilineTextAlignment(.center) - .padding(.horizontal, 20) - UnitButtonView(type: .reload, action: { - self.viewModel.createLessonType() - killPlayer.toggle() - }).frame(width: 100) - }.frame(maxWidth: .infinity, maxHeight: .infinity) - } - - // MARK: - Alert - if showAlert { - ZStack(alignment: .bottomLeading) { - Spacer() - HStack(spacing: 6) { - CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template) - .onAppear { - alertMessage = CourseLocalization.Alert.rotateDevice - } - Text(alertMessage ?? "") - }.shadowCardStyle(bgColor: CoreAssets.accentColor.swiftUIColor, - textColor: .white) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - alertMessage = nil - showAlert = false + } + .offset(y: offsetView) + .clipped() + .onChange(of: viewModel.index, perform: { index in + DispatchQueue.main.async { + withAnimation(Animation.easeInOut(duration: 0.2)) { + offsetView = -(reader.size.height * CGFloat(index)) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + showDiscussion = viewModel.selectedLesson().type == .discussion } } } - } - - // MARK: - Course Navigation - CourseNavigationView( - sectionName: sectionName, - viewModel: viewModel, - killPlayer: $killPlayer - ).padding(.vertical, 12) - .frameLimit(sizePortrait: 420) - .background( - CoreAssets.background.swiftUIColor - .ignoresSafeArea() - .shadow(color: CoreAssets.shadowColor.swiftUIColor, radius: 4, y: -2) - ) - }.frame(maxWidth: .infinity) + + }) + } else { + + // MARK: No internet view + VStack(spacing: 28) { + Image(systemName: "wifi").resizable() + .scaledToFit() + .frame(width: 100) + Text(CourseLocalization.Error.noInternet) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) + UnitButtonView(type: .reload, action: { + playerStateSubject.send(VideoPlayerState.kill) + }).frame(width: 100) + }.frame(maxWidth: .infinity, maxHeight: .infinity) } }.frame(maxWidth: .infinity) - .onRightSwipeGesture { - viewModel.router.back() - killPlayer.toggle() + .clipped() + + // MARK: Progress Dots + if viewModel.verticals[viewModel.verticalIndex].childs.count > 1 { + LessonProgressView(viewModel: viewModel) + } + } + // MARK: - Alert + if showAlert { + ZStack(alignment: .bottomLeading) { + Spacer() + HStack(spacing: 6) { + CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template) + .onAppear { + alertMessage = CourseLocalization.Alert.rotateDevice + } + Text(alertMessage ?? "") + }.shadowCardStyle(bgColor: CoreAssets.accentColor.swiftUIColor, + textColor: .white) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + alertMessage = nil + showAlert = false + } } - + } } - } - .background( - CoreAssets.background.swiftUIColor - .ignoresSafeArea() - ) + + // MARK: - Course Navigation + VStack { + NavigationBar( + title: "", + leftButtonAction: { + viewModel.router.back() + playerStateSubject.send(VideoPlayerState.kill) + }).padding(.top, 50) + Spacer() + CourseNavigationView( + sectionName: sectionName, + viewModel: viewModel, + playerStateSubject: playerStateSubject + ).padding(.bottom, 30) + .frameLimit(sizePortrait: 420) + }.frame(maxWidth: .infinity) + .onRightSwipeGesture { + playerStateSubject.send(VideoPlayerState.kill) + viewModel.router.back() + } + } + }.ignoresSafeArea() + .background( + CoreAssets.background.swiftUIColor + .ignoresSafeArea() + ) } } #if DEBUG //swiftlint:disable all -struct LessonView_Previews: PreviewProvider { +struct CourseUnitView_Previews: PreviewProvider { static var previews: some View { let blocks = [ - CourseBlock(blockId: "1", - id: "1", - topicId: "1", - graded: false, - completion: 0, - type: .vertical, - displayName: "Lesson 1", - studentUrl: "1", - videoUrl: nil, - youTubeUrl: nil), - CourseBlock(blockId: "2", - id: "2", - topicId: "2", - graded: false, + CourseBlock( + blockId: "1", + id: "1", + topicId: "1", + graded: false, + completion: 0, + type: .video, + displayName: "Lesson 1", + studentUrl: "", + videoUrl: nil, + youTubeUrl: nil + ), + CourseBlock( + blockId: "2", + id: "2", + topicId: "2", + graded: false, + completion: 0, + type: .video, + displayName: "Lesson 2", + studentUrl: "2", + videoUrl: nil, + youTubeUrl: nil + ), + CourseBlock( + blockId: "3", + id: "3", + topicId: "3", + graded: false, + completion: 0, + type: .unknown, + displayName: "Lesson 3", + studentUrl: "3", + videoUrl: nil, + youTubeUrl: nil + ), + CourseBlock( + blockId: "4", + id: "4", + topicId: "4", + graded: false, + completion: 0, + type: .unknown, + displayName: "4", + studentUrl: "4", + videoUrl: nil, + youTubeUrl: nil + ), + ] + + let chapters = [ + CourseChapter( + blockId: "0", + id: "0", + displayName: "0", + type: .chapter, + childs: [ + CourseSequential( + blockId: "5", + id: "5", + displayName: "5", + type: .sequential, completion: 0, - type: .chapter, - displayName: "Lesson 2", - studentUrl: "2", - videoUrl: nil, - youTubeUrl: nil), - CourseBlock(blockId: "3", + childs: [ + CourseVertical( + blockId: "6", id: "6", + displayName: "6", + type: .vertical, + completion: 0, + childs: blocks + ) + ] + ) + + ]), + CourseChapter( + blockId: "2", + id: "2", + displayName: "2", + type: .chapter, + childs: [ + CourseSequential( + blockId: "3", id: "3", - topicId: "3", - graded: false, - completion: 0, - type: .vertical, - displayName: "Lesson 3", - studentUrl: "3", - videoUrl: nil, - youTubeUrl: nil), - CourseBlock(blockId: "4", - id: "4", - topicId: "4", - graded: false, + displayName: "3", + type: .sequential, completion: 0, - type: .vertical, - displayName: "4", - studentUrl: "4", - videoUrl: nil, - youTubeUrl: nil), + childs: [ + CourseVertical( + blockId: "4", id: "4", + displayName: "4", + type: .vertical, + completion: 0, + childs: blocks + ) + ] + ) + + ]) ] return CourseUnitView(viewModel: CourseUnitViewModel( - lessonID: "", courseID: "", blocks: blocks, + lessonID: "", + courseID: "", + id: "1", + chapters: chapters, + chapterIndex: 0, + sequentialIndex: 0, + verticalIndex: 0, interactor: CourseInteractor.mock, router: CourseRouterMock(), connectivity: Connectivity(), diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index 2d859ae5c..2705f2e3a 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -5,7 +5,7 @@ // Created by  Stepanok Ivan on 05.10.2022. // -import Foundation +import SwiftUI import Core public enum LessonType: Equatable { @@ -13,7 +13,7 @@ public enum LessonType: Equatable { case youtube(viewYouTubeUrl: String, blockID: String) case video(videoUrl: String, blockID: String) case unknown(String) - case discussion(String) + case discussion(String, String, String) static func from(_ block: CourseBlock) -> Self { switch block.type { @@ -22,7 +22,7 @@ public enum LessonType: Equatable { case .html: return .web(block.studentUrl) case .discussion: - return .discussion(block.topicId ?? "") + return .discussion(block.topicId ?? "", block.id, block.displayName) case .video: if block.youTubeUrl != nil, let encodedVideo = block.videoUrl { return .video(videoUrl: encodedVideo, blockID: block.id) @@ -42,11 +42,17 @@ public enum LessonType: Equatable { public class CourseUnitViewModel: ObservableObject { - public var blocks: [CourseBlock] + enum LessonAction { + case next + case previous + } + + var verticals: [CourseVertical] + var verticalIndex: Int + @Published var index: Int = 0 - @Published var previousLesson: String = "" - @Published var nextLesson: String = "" - @Published var lessonType: LessonType? + var previousLesson: String = "" + var nextLesson: String = "" @Published var showError: Bool = false var errorMessage: String? { didSet { @@ -54,64 +60,65 @@ public class CourseUnitViewModel: ObservableObject { } } - public var lessonID: String - public var courseID: String - + var lessonID: String + var courseID: String + var id: String + private let interactor: CourseInteractorProtocol - public let router: CourseRouter - public let connectivity: ConnectivityProtocol + let router: CourseRouter + let connectivity: ConnectivityProtocol private let manager: DownloadManagerProtocol private var subtitlesDownloaded: Bool = false + let chapters: [CourseChapter] + let chapterIndex: Int + let sequentialIndex: Int func loadIndex() { index = selectLesson() } - public init(lessonID: String, - courseID: String, - blocks: [CourseBlock], - interactor: CourseInteractorProtocol, - router: CourseRouter, - connectivity: ConnectivityProtocol, - manager: DownloadManagerProtocol + public init( + lessonID: String, + courseID: String, + id: String, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int, + verticalIndex: Int, + interactor: CourseInteractorProtocol, + router: CourseRouter, + connectivity: ConnectivityProtocol, + manager: DownloadManagerProtocol ) { self.lessonID = lessonID self.courseID = courseID - self.blocks = blocks + self.id = id + self.chapters = chapters + self.chapterIndex = chapterIndex + self.sequentialIndex = sequentialIndex + self.verticalIndex = verticalIndex + self.verticals = chapters[chapterIndex].childs[sequentialIndex].childs self.interactor = interactor self.router = router self.connectivity = connectivity self.manager = manager } - public func languages() -> [SubtitleUrl] { - return blocks.first(where: { $0.id == lessonID })?.subtitles ?? [] - } - private func selectLesson() -> Int { - guard blocks.count > 0 else { return 0 } - let index = blocks.firstIndex(where: { $0.id == lessonID }) ?? 0 + guard verticals[verticalIndex].childs.count > 0 else { return 0 } + let index = verticals[verticalIndex].childs.firstIndex(where: { $0.id == lessonID }) ?? 0 nextTitles() return index } func selectedLesson() -> CourseBlock { - return blocks[index] - } - - func createLessonType() { - self.lessonType = LessonType.from(blocks[index]) - } - - enum LessonAction { - case next - case previous + return verticals[verticalIndex].childs[index] } func select(move: LessonAction) { switch move { case .next: - if index != blocks.count - 1 { index += 1 } + if index != verticals[verticalIndex].childs.count - 1 { index += 1 } nextTitles() case .previous: if index != 0 { index -= 1 } @@ -121,9 +128,8 @@ public class CourseUnitViewModel: ObservableObject { @MainActor func blockCompletionRequest(blockID: String) async { - let fullBlockID = "block-v1:\(courseID.dropFirst(10))+type@discussion+block@\(blockID)" do { - try await interactor.blockCompletionRequest(courseID: courseID, blockID: fullBlockID) + try await interactor.blockCompletionRequest(courseID: self.id, blockID: blockID) } catch let error { if error.isInternetError || error is NoCachedDataError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection @@ -135,20 +141,18 @@ public class CourseUnitViewModel: ObservableObject { func nextTitles() { if index != 0 { - previousLesson = blocks[index - 1].displayName + previousLesson = verticals[verticalIndex].childs[index - 1].displayName } else { previousLesson = "" } - if index != blocks.count - 1 { - nextLesson = blocks[index + 1].displayName + if index != verticals[verticalIndex].childs.count - 1 { + nextLesson = verticals[verticalIndex].childs[index + 1].displayName } else { nextLesson = "" } } - public func urlForVideoFileOrFallback(blockId: String, url: String) -> URL? { - guard let block = blocks.first(where: { $0.id == blockId }) else { return nil } - + func urlForVideoFileOrFallback(blockId: String, url: String) -> URL? { if let fileURL = manager.fileUrl(for: blockId) { return fileURL } else { diff --git a/Course/Course/Presentation/Unit/Subviews/DiscussionView.swift b/Course/Course/Presentation/Unit/Subviews/DiscussionView.swift new file mode 100644 index 000000000..ed46e69b8 --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/DiscussionView.swift @@ -0,0 +1,37 @@ +// +// DiscussionView.swift +// Course +// +// Created by  Stepanok Ivan on 30.05.2023. +// + +import SwiftUI +import Core +import Discussion +import Swinject + +struct DiscussionView: View { + let id: String + let blockID: String + let blockKey: String + let title: String + let viewModel: CourseUnitViewModel + + var body: some View { + PostsView( + courseID: id, + currentBlockID: blockID, + topics: Topics(coursewareTopics: [], nonCoursewareTopics: []), + title: title, + type: .courseTopics(topicID: blockID), + viewModel: Container.shared.resolve(PostsViewModel.self)!, + router: Container.shared.resolve(DiscussionRouter.self)!, + showTopMenu: false + ) + .onAppear { + Task { + await viewModel.blockCompletionRequest(blockID: blockKey) + } + } + } +} diff --git a/Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift b/Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift new file mode 100644 index 000000000..1bdc629fa --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift @@ -0,0 +1,41 @@ +// +// EncodedVideoView.swift +// Course +// +// Created by  Stepanok Ivan on 30.05.2023. +// + +import SwiftUI +import Core +import Combine +import Swinject + +struct EncodedVideoView: View { + + let name: String + let url: URL? + let courseID: String + let blockID: String + let playerStateSubject: CurrentValueSubject + let languages: [SubtitleUrl] + let isOnScreen: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(name) + .font(Theme.Fonts.titleLarge) + .padding(.horizontal, 24) + + let vm = Container.shared.resolve( + EncodedVideoPlayerViewModel.self, + arguments: url, + blockID, + courseID, + languages, + playerStateSubject + )! + EncodedVideoPlayer(viewModel: vm, isOnScreen: isOnScreen) + Spacer(minLength: 100) + } + } +} diff --git a/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift new file mode 100644 index 000000000..37dcb67d3 --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift @@ -0,0 +1,42 @@ +// +// LessonProgressView.swift +// Course +// +// Created by  Stepanok Ivan on 30.05.2023. +// + +import SwiftUI +import Core + +struct LessonProgressView: View { + @ObservedObject var viewModel: CourseUnitViewModel + + init(viewModel: CourseUnitViewModel) { + self.viewModel = viewModel + } + + var body: some View { + HStack { + Spacer() + VStack { + Spacer() + let childs = viewModel.verticals[viewModel.verticalIndex].childs + ForEach(Array(childs.enumerated()), id: \.offset) { index, _ in + let selected = viewModel.verticals[viewModel.verticalIndex].childs[index] + Circle() + .frame( + width: selected == viewModel.selectedLesson() ? 5 : 3, + height: selected == viewModel.selectedLesson() ? 5 : 3 + ) + .foregroundColor( + selected == viewModel.selectedLesson() + ? .accentColor + : CoreAssets.textSecondary.swiftUIColor + ) + } + Spacer() + } + .padding(.trailing, 6) + } + } +} diff --git a/Course/Course/Presentation/Unit/Subviews/UnknownView.swift b/Course/Course/Presentation/Unit/Subviews/UnknownView.swift new file mode 100644 index 000000000..4f25de9da --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/UnknownView.swift @@ -0,0 +1,38 @@ +// +// UnknownView.swift +// Course +// +// Created by  Stepanok Ivan on 30.05.2023. +// + +import SwiftUI +import Core + +struct UnknownView: View { + let url: String + let viewModel: CourseUnitViewModel + + var body: some View { + VStack(spacing: 0) { + CoreAssets.notAvaliable.swiftUIImage + Text(CourseLocalization.NotAvaliable.title) + .font(Theme.Fonts.titleLarge) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.top, 40) + Text(CourseLocalization.NotAvaliable.description) + .font(Theme.Fonts.bodyLarge) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.top, 12) + StyledButton(CourseLocalization.NotAvaliable.button, action: { + if let url = URL(string: url) { + UIApplication.shared.open(url) + } + }) + .frame(width: 215) + .padding(.top, 40) + } + .padding(24) + } +} diff --git a/Course/Course/Presentation/Unit/Subviews/WebView.swift b/Course/Course/Presentation/Unit/Subviews/WebView.swift new file mode 100644 index 000000000..9cdc59269 --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/WebView.swift @@ -0,0 +1,23 @@ +// +// WebView.swift +// Course +// +// Created by  Stepanok Ivan on 30.05.2023. +// + +import SwiftUI +import Swinject +import Core + +struct WebView: View { + let url: String + let viewModel: CourseUnitViewModel + + var body: some View { + VStack(spacing: 0) { + WebUnitView(url: url, viewModel: Container.shared.resolve(WebUnitViewModel.self)!) + Spacer(minLength: 5) + } + .roundedBackground(strokeColor: .clear, maxIpadWidth: .infinity) + } +} diff --git a/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift b/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift new file mode 100644 index 000000000..8aeab7f13 --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift @@ -0,0 +1,43 @@ +// +// YouTubeView.swift +// Course +// +// Created by  Stepanok Ivan on 30.05.2023. +// + +import SwiftUI +import Core +import Combine +import Swinject + +struct YouTubeView: View { + + let name: String + let url: String + let courseID: String + let blockID: String + let playerStateSubject: CurrentValueSubject + let languages: [SubtitleUrl] + let isOnScreen: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading) { + Text(name) + .font(Theme.Fonts.titleLarge) + .padding(.horizontal, 24) + + let vm = Container.shared.resolve( + YouTubeVideoPlayerViewModel.self, + arguments: url, + blockID, + courseID, + languages, + playerStateSubject + )! + YouTubeVideoPlayer(viewModel: vm, isOnScreen: isOnScreen) + Spacer(minLength: 100) + }.background(CoreAssets.background.swiftUIColor) + } + } +} diff --git a/Course/Course/Presentation/Unit/UnitButtonView.swift b/Course/Course/Presentation/Unit/UnitButtonView.swift deleted file mode 100644 index b2494aa80..000000000 --- a/Course/Course/Presentation/Unit/UnitButtonView.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// UnitButtonView.swift -// Course -// -// Created by  Stepanok Ivan on 14.02.2023. -// - -import SwiftUI -import Core - -struct UnitButtonView: View { - - enum UnitButtonType { - case first - case next - case previous - case last - case finish - case reload - case continueLesson - - func stringValue() -> String { - switch self { - case .first: - return CourseLocalization.Courseware.next - case .next: - return CourseLocalization.Courseware.next - case .previous: - return CourseLocalization.Courseware.previous - case .last: - return CourseLocalization.Courseware.finish - case .finish: - return CourseLocalization.Courseware.finish - case .reload: - return CourseLocalization.Error.reload - case .continueLesson: - return CourseLocalization.Courseware.continue - } - } - } - - private let action: () -> Void - private let type: UnitButtonType - - init(type: UnitButtonType, action: @escaping () -> Void) { - self.action = action - self.type = type - } - - var body: some View { - HStack { - Button(action: action) { - VStack { - switch type { - case .first: - HStack { - Text(type.stringValue()) - .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) - .font(Theme.Fonts.labelLarge) - CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) - .rotationEffect(Angle.degrees(180)) - } - case .next: - HStack { - Text(type.stringValue()) - .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) - .padding(.leading, 20) - .font(Theme.Fonts.labelLarge) - Spacer() - CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) - .rotationEffect(Angle.degrees(180)) - .padding(.trailing, 20) - } - case .previous: - HStack { - CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .padding(.leading, 20) - .foregroundColor(CoreAssets.accentColor.swiftUIColor) - Spacer() - Text(type.stringValue()) - .foregroundColor(CoreAssets.accentColor.swiftUIColor) - .font(Theme.Fonts.labelLarge) - .padding(.trailing, 20) - } - case .last: - HStack { - Text(type.stringValue()) - .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) - .padding(.leading, 16) - .font(Theme.Fonts.labelLarge) - Spacer() - CoreAssets.check.swiftUIImage.renderingMode(.template) - .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) - .padding(.trailing, 16) - } - case .finish: - HStack { - Text(type.stringValue()) - .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) - .font(Theme.Fonts.labelLarge) - CoreAssets.check.swiftUIImage.renderingMode(.template) - .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) - } - case .reload: - VStack(alignment: .center) { - Text(type.stringValue()) - .foregroundColor(CoreAssets.accentColor.swiftUIColor) - .font(Theme.Fonts.labelLarge) - } - case .continueLesson: - HStack { - Text(type.stringValue()) - .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) - .padding(.leading, 20) - .font(Theme.Fonts.labelLarge) - CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) - .rotationEffect(Angle.degrees(180)) - .padding(.trailing, 20) - } - } - } - .frame(maxWidth: .infinity, minHeight: 48) - .background( - VStack { - if self.type == .reload { - Theme.Shapes.buttonShape - .fill(.clear) - } else { - Theme.Shapes.buttonShape - .fill(type == .previous ? .clear : CoreAssets.accentColor.swiftUIColor) - } - } - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) - .foregroundColor(CoreAssets.accentColor.swiftUIColor) - ) - } - } - } -} - -struct UnitButtonView_Previews: PreviewProvider { - static var previews: some View { - VStack { - UnitButtonView(type: .first, action: {}) - UnitButtonView(type: .previous, action: {}) - UnitButtonView(type: .next, action: {}) - UnitButtonView(type: .last, action: {}) - UnitButtonView(type: .finish, action: {}) - UnitButtonView(type: .reload, action: {}) - UnitButtonView(type: .continueLesson, action: {}) - } - } -} diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index 0eaaad718..a3ddda18f 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -9,17 +9,20 @@ import SwiftUI import _AVKit_SwiftUI import Core import Swinject +import Combine + +public enum VideoPlayerState { + case pause + case kill +} public struct EncodedVideoPlayer: View { - @ObservedObject - private var viewModel = Container.shared.resolve(VideoPlayerViewModel.self)! + @StateObject + private var viewModel: EncodedVideoPlayerViewModel - private var blockID: String - private var courseID: String - private let languages: [SubtitleUrl] + private var isOnScreen: Bool - private var controller = AVPlayerViewController() private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @State private var orientation = UIDevice.current.orientation @State private var isLoading: Bool = true @@ -27,7 +30,7 @@ public struct EncodedVideoPlayer: View { @State private var isViewedOnce: Bool = false @State private var currentTime: Double = 0 @State private var isOrientationChanged: Bool = false - @Binding private var killPlayer: Bool + @State var showAlert = false @State var alertMessage: String? { didSet { @@ -36,33 +39,26 @@ public struct EncodedVideoPlayer: View { } } } - private let url: URL? public init( - url: URL?, - blockID: String, - courseID: String, - languages: [SubtitleUrl], - killPlayer: Binding + viewModel: EncodedVideoPlayerViewModel, + isOnScreen: Bool ) { - self.url = url - self.blockID = blockID - self.courseID = courseID - self.languages = languages - self._killPlayer = killPlayer + self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.isOnScreen = isOnScreen } public var body: some View { ZStack { VStack(alignment: .leading) { PlayerViewController( - videoURL: url, - controller: controller, + videoURL: viewModel.url, + controller: viewModel.controller, progress: { progress in if progress >= 0.8 { if !isViewedOnce { Task { - await viewModel.blockCompletionRequest(blockID: blockID, courseID: courseID) + await viewModel.blockCompletionRequest() } isViewedOnce = true } @@ -75,23 +71,26 @@ public struct EncodedVideoPlayer: View { .padding(.horizontal, 6) .onReceive(NotificationCenter.Publisher( center: .default, - name: UIDevice.orientationDidChangeNotification)) { _ in + name: UIDevice.orientationDidChangeNotification) + ) { _ in + if isOnScreen { self.orientation = UIDevice.current.orientation if self.orientation.isLandscape { - controller.enterFullScreen(animated: true) - controller.player?.play() + viewModel.controller.enterFullScreen(animated: true) + viewModel.controller.player?.play() isOrientationChanged = true } else { if isOrientationChanged { - controller.exitFullScreen(animated: true) - controller.player?.pause() + viewModel.controller.exitFullScreen(animated: true) + viewModel.controller.player?.pause() isOrientationChanged = false } } } - SubtittlesView(languages: languages, - currentTime: $currentTime, - viewModel: viewModel) + } + SubtittlesView(languages: viewModel.languages, + currentTime: $currentTime, + viewModel: viewModel) Spacer() if !orientation.isLandscape || idiom != .pad { VStack {}.onAppear { @@ -99,23 +98,21 @@ public struct EncodedVideoPlayer: View { alertMessage = CourseLocalization.Alert.rotateDevice } } - }.onChange(of: killPlayer, perform: { _ in - controller.player?.pause() - controller.player?.replaceCurrentItem(with: nil) - }) + } + // MARK: - Alert - if showAlert { + if showAlert, let alertMessage { VStack(alignment: .center) { Spacer() HStack(spacing: 6) { CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template) - Text(alertMessage ?? "") + Text(alertMessage) }.shadowCardStyle(bgColor: CoreAssets.snackbarInfoAlert.swiftUIColor, textColor: .white) .transition(.move(edge: .bottom)) .onAppear { doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - alertMessage = nil + self.alertMessage = nil showAlert = false } } @@ -125,8 +122,22 @@ public struct EncodedVideoPlayer: View { } } +#if DEBUG struct EncodedVideoPlayer_Previews: PreviewProvider { static var previews: some View { - EncodedVideoPlayer(url: nil, blockID: "", courseID: "", languages: [], killPlayer: .constant(false)) + EncodedVideoPlayer( + viewModel: EncodedVideoPlayerViewModel( + url: URL(string: "")!, + blockID: "", + courseID: "", + languages: [], + playerStateSubject: CurrentValueSubject(nil), + interactor: CourseInteractor(repository: CourseRepositoryMock()), + router: CourseRouterMock(), + connectivity: Connectivity() + ), + isOnScreen: true + ) } } +#endif diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift new file mode 100644 index 000000000..b75a57384 --- /dev/null +++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift @@ -0,0 +1,49 @@ +// +// EncodedVideoPlayerViewModel.swift +// Course +// +// Created by  Stepanok Ivan on 24.05.2023. +// + +import _AVKit_SwiftUI +import Core +import Combine + +public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { + + let url: URL? + + let controller = AVPlayerViewController() + private var subscription = Set() + + public init( + url: URL?, + blockID: String, + courseID: String, + languages: [SubtitleUrl], + playerStateSubject: CurrentValueSubject, + interactor: CourseInteractorProtocol, + router: CourseRouter, + connectivity: ConnectivityProtocol + ) { + self.url = url + + super.init(blockID: blockID, + courseID: courseID, + languages: languages, + interactor: interactor, + router: router, + connectivity: connectivity) + + playerStateSubject.sink(receiveValue: { [weak self] state in + switch state { + case .pause: + self?.controller.player?.pause() + case .kill: + self?.controller.player?.replaceCurrentItem(with: nil) + case .none: + break + } + }).store(in: &subscription) + } +} diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift index e67143ca8..ef856ff04 100644 --- a/Course/Course/Presentation/Video/PlayerViewController.swift +++ b/Course/Course/Presentation/Video/PlayerViewController.swift @@ -12,12 +12,14 @@ struct PlayerViewController: UIViewControllerRepresentable { var videoURL: URL? var controller: AVPlayerViewController - public var progress: ((Float) -> Void) - public var seconds: ((Double) -> Void) + var progress: ((Float) -> Void) + var seconds: ((Double) -> Void) - init(videoURL: URL?, controller: AVPlayerViewController, - progress: @escaping ((Float) -> Void), - seconds: @escaping ((Double) -> Void)) { + init( + videoURL: URL?, controller: AVPlayerViewController, + progress: @escaping ((Float) -> Void), + seconds: @escaping ((Double) -> Void) + ) { self.videoURL = videoURL self.controller = controller self.progress = progress @@ -27,33 +29,45 @@ struct PlayerViewController: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> AVPlayerViewController { controller.modalPresentationStyle = .fullScreen controller.allowsPictureInPicturePlayback = true + controller.player = AVPlayer() - addPeriodicTimeObserver(controller, currentProgress: { progress, seconds in - self.progress(progress) - self.seconds(seconds) - }) + addPeriodicTimeObserver( + controller, + currentProgress: { progress, seconds in + self.progress(progress) + self.seconds(seconds) + } + ) return controller } - private func addPeriodicTimeObserver(_ controller: AVPlayerViewController, - currentProgress: @escaping ((Float, Double) -> Void)) { - let interval = CMTime(seconds: 0.1, - preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + private func addPeriodicTimeObserver( + _ controller: AVPlayerViewController, + currentProgress: @escaping ((Float, Double) -> Void) + ) { + let interval = CMTime( + seconds: 0.1, + preferredTimescale: CMTimeScale(NSEC_PER_SEC) + ) self.controller.player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in var progress: Float = .zero let currentSeconds = CMTimeGetSeconds(time) guard let duration = controller.player?.currentItem?.duration else { return } let totalSeconds = CMTimeGetSeconds(duration) - progress = Float(currentSeconds/totalSeconds) + progress = Float(currentSeconds / totalSeconds) currentProgress(progress, currentSeconds) } } func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) { DispatchQueue.main.async { - if (playerController.player?.currentItem?.asset as? AVURLAsset)?.url.absoluteString != videoURL?.absoluteString { - playerController.player = AVPlayer(url: videoURL!) + let asset = playerController.player?.currentItem?.asset as? AVURLAsset + if asset?.url.absoluteString != videoURL?.absoluteString { + if playerController.player == nil { + playerController.player = AVPlayer() + } + playerController.player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL!)) addPeriodicTimeObserver(playerController, currentProgress: { progress, seconds in self.progress(progress) self.seconds(seconds) diff --git a/Course/Course/Presentation/Video/SubtittlesView.swift b/Course/Course/Presentation/Video/SubtittlesView.swift index 493ab4f98..d09e89967 100644 --- a/Course/Course/Presentation/Video/SubtittlesView.swift +++ b/Course/Course/Presentation/Video/SubtittlesView.swift @@ -50,40 +50,41 @@ public struct SubtittlesView: View { }) } } - ScrollView { - if viewModel.subtitles.count > 0 { - VStack(alignment: .leading, spacing: 0) { - ForEach(viewModel.subtitles, id: \.id) { subtitle in - HStack { - Text(subtitle.text) - .padding(.vertical, 16) - .font(Theme.Fonts.bodyMedium) - .foregroundColor(subtitle.fromTo.contains(Date(milliseconds: currentTime)) - ? CoreAssets.textPrimary.swiftUIColor - : CoreAssets.textSecondary.swiftUIColor) - .onChange(of: currentTime, perform: { _ in - if subtitle.fromTo.contains(Date(milliseconds: currentTime)) { - if id != subtitle.id { - withAnimation { - scroll.scrollTo(subtitle.id, anchor: .top) + ZStack { + ScrollView { + if viewModel.subtitles.count > 0 { + VStack(alignment: .leading, spacing: 0) { + ForEach(viewModel.subtitles, id: \.id) { subtitle in + HStack { + Text(subtitle.text) + .padding(.vertical, 16) + .font(Theme.Fonts.bodyMedium) + .foregroundColor(subtitle.fromTo.contains(Date(milliseconds: currentTime)) + ? CoreAssets.textPrimary.swiftUIColor + : CoreAssets.textSecondary.swiftUIColor) + .onChange(of: currentTime, perform: { _ in + if subtitle.fromTo.contains(Date(milliseconds: currentTime)) { + if id != subtitle.id { + withAnimation { + scroll.scrollTo(subtitle.id, anchor: .top) + } } + self.id = subtitle.id } - self.id = subtitle.id - } - }) - }.id(subtitle.id) + }) + }.id(subtitle.id) + } } + .introspectScrollView(customize: { scroll in + scroll.isScrollEnabled = false + }) } } - }.introspectScrollView(customize: { scroll in - scroll.isScrollEnabled = false - }) + // Forced disable scrolling for iOS 14, 15 + Color.white.opacity(0) + } }.padding(.horizontal, 24) .padding(.top, 34) - .onAppear { - viewModel.languages = languages - viewModel.prepareLanguages() - } } } } @@ -92,12 +93,18 @@ public struct SubtittlesView: View { struct SubtittlesView_Previews: PreviewProvider { static var previews: some View { - SubtittlesView(languages: [SubtitleUrl(language: "fr", url: "url"), - SubtitleUrl(language: "uk", url: "url2")], - currentTime: .constant(0), - viewModel: VideoPlayerViewModel(interactor: CourseInteractor(repository: CourseRepositoryMock()), - router: CourseRouterMock(), - connectivity: Connectivity())) + SubtittlesView( + languages: [SubtitleUrl(language: "fr", url: "url"), + SubtitleUrl(language: "uk", url: "url2")], + currentTime: .constant(0), + viewModel: VideoPlayerViewModel( + blockID: "", courseID: "", + languages: [], + interactor: CourseInteractor(repository: CourseRepositoryMock()), + router: CourseRouterMock(), + connectivity: Connectivity() + ) + ) } } #endif diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index 021730358..7aab1c567 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -7,16 +7,20 @@ import Foundation import Core +import _AVKit_SwiftUI public class VideoPlayerViewModel: ObservableObject { + private var blockID: String + private var courseID: String + private let interactor: CourseInteractorProtocol public let connectivity: ConnectivityProtocol public let router: CourseRouter private var subtitlesDownloaded: Bool = false @Published var subtitles: [Subtitle] = [] - @Published var languages: [SubtitleUrl] = [] + var languages: [SubtitleUrl] @Published var items: [PickerItem] = [] @Published var selectedLanguage: String? @@ -27,16 +31,25 @@ public class VideoPlayerViewModel: ObservableObject { } } - public init(interactor: CourseInteractorProtocol, - router: CourseRouter, - connectivity: ConnectivityProtocol) { + public init( + blockID: String, + courseID: String, + languages: [SubtitleUrl], + interactor: CourseInteractorProtocol, + router: CourseRouter, + connectivity: ConnectivityProtocol + ) { + self.blockID = blockID + self.courseID = courseID + self.languages = languages self.interactor = interactor self.router = router self.connectivity = connectivity + self.prepareLanguages() } @MainActor - func blockCompletionRequest(blockID: String, courseID: String) async { + func blockCompletionRequest() async { let fullBlockID = "block-v1:\(courseID.dropFirst(10))+type@discussion+block@\(blockID)" do { try await interactor.blockCompletionRequest(courseID: courseID, blockID: fullBlockID) @@ -51,8 +64,15 @@ public class VideoPlayerViewModel: ObservableObject { @MainActor public func getSubtitles(subtitlesUrl: String) async { - guard let result = try? await interactor.getSubtitles(url: subtitlesUrl) else { return } - subtitles = result + do { + let result = try await interactor.getSubtitles( + url: subtitlesUrl, + selectedLanguage: self.selectedLanguage ?? "en" + ) + subtitles = result + } catch { + print(">>>>> ⛔️⛔️⛔️⛔️⛔️⛔️⛔️⛔️", error) + } } public func prepareLanguages() { diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index 3662ba357..b8cc5d335 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -13,161 +13,83 @@ import Swinject public struct YouTubeVideoPlayer: View { - private let viewModel = Container.shared.resolve(VideoPlayerViewModel.self)! + @StateObject + private var viewModel: YouTubeVideoPlayerViewModel + private var isOnScreen: Bool - private var blockID: String - private var courseID: String - private let languages: [SubtitleUrl] - - private let youtubePlayer: YouTubePlayer - private var timePublisher: AnyPublisher - private var durationPublisher: AnyPublisher - private var currentTimePublisher: AnyPublisher - private var currentStatePublisher: AnyPublisher - private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - - @State private var duration: Double? - @State private var play = false - @State private var orientation = UIDevice.current.orientation - @State private var isLoading: Bool = true - @State private var isViewedOnce: Bool = false - @State private var currentTime: Double = 0 - @State private var showAlert = false - @State private var alertMessage: String? { + @State + private var showAlert = false + @State + private var alertMessage: String? { didSet { withAnimation { showAlert = alertMessage != nil } } } - - public init(url: String, - blockID: String, - courseID: String, - languages: [SubtitleUrl] - ) { - self.blockID = blockID - self.courseID = courseID - self.languages = languages - - let videoID = url.replacingOccurrences(of: "https://www.youtube.com/watch?v=", with: "") - let configuration = YouTubePlayer.Configuration(configure: { - $0.autoPlay = false - $0.playInline = true - $0.showFullscreenButton = true - $0.allowsPictureInPictureMediaPlayback = false - $0.showControls = true - $0.useModestBranding = false - $0.progressBarColor = .white - $0.showRelatedVideos = false - $0.showCaptions = false - $0.showAnnotations = false - $0.customUserAgent = """ - Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; ja-jp) - AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5 - """ - }) - self.youtubePlayer = YouTubePlayer(source: .video(id: videoID), - configuration: configuration) - self.timePublisher = youtubePlayer.currentTimePublisher() - self.durationPublisher = youtubePlayer.durationPublisher - self.currentTimePublisher = youtubePlayer.currentTimePublisher(updateInterval: 0.1) - self.currentStatePublisher = youtubePlayer.playbackStatePublisher + + public init(viewModel: YouTubeVideoPlayerViewModel, isOnScreen: Bool) { + self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.isOnScreen = isOnScreen } - + public var body: some View { ZStack { VStack { YouTubePlayerView( - youtubePlayer, + viewModel.youtubePlayer, transaction: .init(animation: .easeIn), - overlay: { state in - if state == .ready { - if idiom == .pad { - VStack {}.onAppear { - isLoading = false - } - } else { - VStack {}.onAppear { - isLoading = false - alertMessage = CourseLocalization.Alert.rotateDevice - } - } - } - }) + overlay: { _ in }) + .onAppear { + alertMessage = CourseLocalization.Alert.rotateDevice + } .cornerRadius(12) .padding(.horizontal, 6) - .aspectRatio(16/8.8, contentMode: .fit) - .onReceive(NotificationCenter - .Publisher(center: .default, - name: UIDevice.orientationDidChangeNotification)) { _ in - self.orientation = UIDevice.current.orientation - if self.orientation.isPortrait { - youtubePlayer.update(configuration: YouTubePlayer.Configuration(configure: { - $0.playInline = true - $0.autoPlay = play - $0.startTime = Int(currentTime) - })) - } else { - youtubePlayer.update(configuration: YouTubePlayer.Configuration(configure: { - $0.playInline = false - $0.autoPlay = true - $0.startTime = Int(currentTime) - })) - } - } - SubtittlesView(languages: languages, - currentTime: $currentTime, - viewModel: viewModel) - - }.onReceive(durationPublisher, perform: { duration in - self.duration = duration - }) - .onReceive(currentTimePublisher, perform: { time in - currentTime = time - }) - .onReceive(currentStatePublisher, perform: { state in - switch state { - case .unstarted: - self.play = false - case .ended: - self.play = false - case .playing: - self.play = true - case .paused: - self.play = false - case .buffering, .cued: - break - } - }) - .onReceive(timePublisher, perform: { time in - if let duration { - if (time / duration) >= 0.8 { - if !isViewedOnce { - Task { - await viewModel.blockCompletionRequest(blockID: blockID, courseID: courseID) - } - isViewedOnce = true + .aspectRatio(16 / 8.8, contentMode: .fit) + .onReceive(NotificationCenter.Publisher( + center: .default, name: UIDevice.orientationDidChangeNotification + )) { _ in + if isOnScreen { + let orientation = UIDevice.current.orientation + if orientation.isPortrait { + viewModel.youtubePlayer.update(configuration: YouTubePlayer.Configuration(configure: { + $0.playInline = true + $0.autoPlay = viewModel.play + $0.startTime = Int(viewModel.currentTime) + })) + } else { + viewModel.youtubePlayer.update(configuration: YouTubePlayer.Configuration(configure: { + $0.playInline = false + $0.autoPlay = true + $0.startTime = Int(viewModel.currentTime) + })) } } } - }) - if isLoading { + SubtittlesView( + languages: viewModel.languages, + currentTime: $viewModel.currentTime, + viewModel: viewModel + ) + } + + if viewModel.isLoading { ProgressBar(size: 40, lineWidth: 8) } + // MARK: - Alert - if showAlert { + if showAlert, let alertMessage { VStack(alignment: .center) { Spacer() HStack(spacing: 6) { CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template) - Text(alertMessage ?? "") + Text(alertMessage) }.shadowCardStyle(bgColor: CoreAssets.snackbarInfoAlert.swiftUIColor, textColor: .white) .transition(.move(edge: .bottom)) .onAppear { doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - alertMessage = nil + self.alertMessage = nil showAlert = false } } @@ -177,11 +99,20 @@ public struct YouTubeVideoPlayer: View { } } +#if DEBUG struct YouTubeVideoPlayer_Previews: PreviewProvider { static var previews: some View { - YouTubeVideoPlayer(url: "", - blockID: "", - courseID: "", - languages: []) + YouTubeVideoPlayer( + viewModel: YouTubeVideoPlayerViewModel( + url: "", + blockID: "", + courseID: "", + languages: [], + playerStateSubject: CurrentValueSubject(nil), + interactor: CourseInteractor(repository: CourseRepositoryMock()), + router: CourseRouterMock(), + connectivity: Connectivity()), + isOnScreen: true) } } +#endif diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift new file mode 100644 index 000000000..5aaacf7ff --- /dev/null +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift @@ -0,0 +1,124 @@ +// +// YouTubeVideoPlayerViewModel.swift +// Course +// +// Created by  Stepanok Ivan on 24.05.2023. +// + +import SwiftUI +import Core +import YouTubePlayerKit +import Combine +import Swinject + +public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { + + @Published var youtubePlayer: YouTubePlayer + private (set) var play = false + @Published var isLoading: Bool = true + @Published var currentTime: Double = 0 + + private var subscription = Set() + private var duration: Double? + private var isViewedOnce: Bool = false + private var url: String + + public init( + url: String, + blockID: String, + courseID: String, + languages: [SubtitleUrl], + playerStateSubject: CurrentValueSubject, + interactor: CourseInteractorProtocol, + router: CourseRouter, + connectivity: ConnectivityProtocol + ) { + self.url = url + + let videoID = url.replacingOccurrences(of: "https://www.youtube.com/watch?v=", with: "") + let configuration = YouTubePlayer.Configuration(configure: { + $0.autoPlay = false + $0.playInline = true + $0.showFullscreenButton = true + $0.allowsPictureInPictureMediaPlayback = false + $0.showControls = true + $0.useModestBranding = false + $0.progressBarColor = .white + $0.showRelatedVideos = false + $0.showCaptions = false + $0.showAnnotations = false + $0.customUserAgent = """ + Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; ja-jp) + AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5 + """ + }) + self.youtubePlayer = YouTubePlayer(source: .video(id: videoID), configuration: configuration) + + super.init( + blockID: blockID, + courseID: courseID, + languages: languages, + interactor: interactor, + router: router, + connectivity: connectivity + ) + + self.youtubePlayer.pause() + + subscrube(playerStateSubject: playerStateSubject) + } + + private func subscrube(playerStateSubject: CurrentValueSubject) { + playerStateSubject.sink(receiveValue: { [weak self] state in + switch state { + case .pause: + self?.youtubePlayer.pause() + case .kill, .none: + break + } + }).store(in: &subscription) + + youtubePlayer.durationPublisher.sink(receiveValue: { [weak self] duration in + self?.duration = duration + }).store(in: &subscription) + + youtubePlayer.currentTimePublisher(updateInterval: 0.1).sink(receiveValue: { [weak self] time in + guard let self else { return } + self.currentTime = time + + if let duration = self.duration { + if (time / duration) >= 0.8 { + if !isViewedOnce { + Task { + await self.blockCompletionRequest() + } + isViewedOnce = true + } + } + } + }).store(in: &subscription) + + youtubePlayer.playbackStatePublisher.sink(receiveValue: { [weak self] state in + guard let self else { return } + switch state { + case .unstarted: + self.play = false + case .ended: + self.play = false + case .playing: + self.play = true + case .paused: + self.play = false + case .buffering, .cued: + break + } + }).store(in: &subscription) + + youtubePlayer.statePublisher.sink(receiveValue: { [weak self] state in + guard let self else { return } + if state == .ready { + self.isLoading = false + } + }).store(in: &subscription) + } +} diff --git a/Course/Course/SwiftGen/Strings.swift b/Course/Course/SwiftGen/Strings.swift index 85d149d8b..f5719bf9d 100644 --- a/Course/Course/SwiftGen/Strings.swift +++ b/Course/Course/SwiftGen/Strings.swift @@ -29,14 +29,14 @@ public enum CourseLocalization { public static let finish = CourseLocalization.tr("Localizable", "COURSEWARE.FINISH", fallback: "Finish") /// Good Work! public static let goodWork = CourseLocalization.tr("Localizable", "COURSEWARE.GOOD_WORK", fallback: "Good Work!") - /// is finished. - public static let isFinished = CourseLocalization.tr("Localizable", "COURSEWARE.IS_FINISHED", fallback: "is finished.") + /// “ is finished. + public static let isFinished = CourseLocalization.tr("Localizable", "COURSEWARE.IS_FINISHED", fallback: "“ is finished.") /// Next public static let next = CourseLocalization.tr("Localizable", "COURSEWARE.NEXT", fallback: "Next") - /// Previous - public static let previous = CourseLocalization.tr("Localizable", "COURSEWARE.PREVIOUS", fallback: "Previous") - /// Section - public static let section = CourseLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "Section") + /// Prev + public static let previous = CourseLocalization.tr("Localizable", "COURSEWARE.PREVIOUS", fallback: "Prev") + /// Section “ + public static let section = CourseLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "Section “") } public enum CourseContainer { /// Course diff --git a/Course/Course/en.lproj/Localizable.strings b/Course/Course/en.lproj/Localizable.strings index 0db063144..0f5edc88f 100644 --- a/Course/Course/en.lproj/Localizable.strings +++ b/Course/Course/en.lproj/Localizable.strings @@ -21,12 +21,12 @@ "COURSEWARE.COURSE_CONTENT" = "Course content"; "COURSEWARE.COURSE_UNITS" = "Course units"; "COURSEWARE.NEXT" = "Next"; -"COURSEWARE.PREVIOUS" = "Previous"; +"COURSEWARE.PREVIOUS" = "Prev"; "COURSEWARE.FINISH" = "Finish"; "COURSEWARE.GOOD_WORK" = "Good Work!"; "COURSEWARE.BACK_TO_OUTLINE" = "Back to outline"; -"COURSEWARE.SECTION" = "Section"; -"COURSEWARE.IS_FINISHED" = "is finished."; +"COURSEWARE.SECTION" = "Section “"; +"COURSEWARE.IS_FINISHED" = "“ is finished."; "COURSEWARE.CONTINUE" = "Continue"; "COURSEWARE.CONTINUE_WITH" = "Continue with:"; diff --git a/Course/Course/uk.lproj/Localizable.strings b/Course/Course/uk.lproj/Localizable.strings index f3cb347c4..cedc987f1 100644 --- a/Course/Course/uk.lproj/Localizable.strings +++ b/Course/Course/uk.lproj/Localizable.strings @@ -24,8 +24,8 @@ "COURSEWARE.FINISH" = "Завершити"; "COURSEWARE.GOOD_WORK" = "Гарна робота!"; "COURSEWARE.BACK_TO_OUTLINE" = "Повернутись до модуля"; -"COURSEWARE.SECTION" = "Секція"; -"COURSEWARE.IS_FINISHED" = "завершена."; +"COURSEWARE.SECTION" = "Секція “"; +"COURSEWARE.IS_FINISHED" = "“ завершена."; "COURSEWARE.CONTINUE" = "Продовжити"; "COURSEWARE.CONTINUE_WITH" = "Продовжити далі:"; diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index 7993fb40e..e909b96b1 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -514,10 +514,10 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`) } - open func presentAlert(alertTitle: String, alertMessage: String, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void) { - addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) - let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) as? (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void - perform?(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`) + open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void + perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`) } open func presentView(transitionStyle: UIModalTransitionStyle, view: any View) { @@ -544,7 +544,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showRegisterScreen case m_showForgotPasswordScreen case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) - case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) case m_presentView__transitionStyle_transitionStylecontent_content(Parameter, Parameter<() -> any View>) @@ -590,14 +590,16 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) return Matcher.ComparisonResult(results) - case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped)): + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsNextsectionname, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped, let lhsNextsectiontapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsNextsectionname, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped, let rhsNextsectiontapped)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectionname, rhs: rhsNextsectionname, with: matcher), lhsNextsectionname, rhsNextsectionname, "nextSectionName")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsImage, rhs: rhsImage, with: matcher), lhsImage, rhsImage, "image")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectiontapped, rhs: rhsNextsectiontapped, with: matcher), lhsNextsectiontapped, rhsNextsectiontapped, "nextSectionTapped")) return Matcher.ComparisonResult(results) case (.m_presentView__transitionStyle_transitionStyleview_view(let lhsTransitionstyle, let lhsView), .m_presentView__transitionStyle_transitionStyleview_view(let rhsTransitionstyle, let rhsView)): @@ -627,7 +629,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return 0 case .m_showForgotPasswordScreen: return 0 case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue - case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue case let .m_presentView__transitionStyle_transitionStylecontent_content(p0, p1): return p0.intValue + p1.intValue } @@ -644,7 +646,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return ".showRegisterScreen()" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" - case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped: return ".presentAlert(alertTitle:alertMessage:action:image:onCloseTapped:okTapped:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" case .m_presentView__transitionStyle_transitionStylecontent_content: return ".presentView(transitionStyle:content:)" } @@ -675,7 +677,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} public static func presentView(transitionStyle: Parameter, content: Parameter<() -> any View>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStylecontent_content(`transitionStyle`, `content`))} } @@ -714,8 +716,8 @@ open class BaseRouterMock: BaseRouter, Mock { public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, perform: @escaping (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { - return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`), performs: perform) + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>, perform: @escaping (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`), performs: perform) } public static func presentView(transitionStyle: Parameter, view: Parameter, perform: @escaping (UIModalTransitionStyle, any View) -> Void) -> Perform { return Perform(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`), performs: perform) @@ -1192,16 +1194,16 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { return __value } - open func getSubtitles(url: String) throws -> [Subtitle] { - addInvocation(.m_getSubtitles__url_url(Parameter.value(`url`))) - let perform = methodPerformValue(.m_getSubtitles__url_url(Parameter.value(`url`))) as? (String) -> Void - perform?(`url`) + open func getSubtitles(url: String, selectedLanguage: String) throws -> [Subtitle] { + addInvocation(.m_getSubtitles__url_urlselectedLanguage_selectedLanguage(Parameter.value(`url`), Parameter.value(`selectedLanguage`))) + let perform = methodPerformValue(.m_getSubtitles__url_urlselectedLanguage_selectedLanguage(Parameter.value(`url`), Parameter.value(`selectedLanguage`))) as? (String, String) -> Void + perform?(`url`, `selectedLanguage`) var __value: [Subtitle] do { - __value = try methodReturnValue(.m_getSubtitles__url_url(Parameter.value(`url`))).casted() + __value = try methodReturnValue(.m_getSubtitles__url_urlselectedLanguage_selectedLanguage(Parameter.value(`url`), Parameter.value(`selectedLanguage`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getSubtitles(url: String). Use given") - Failure("Stub return value not specified for getSubtitles(url: String). Use given") + onFatalFailure("Stub return value not specified for getSubtitles(url: String, selectedLanguage: String). Use given") + Failure("Stub return value not specified for getSubtitles(url: String, selectedLanguage: String). Use given") } catch { throw error } @@ -1220,7 +1222,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { case m_getHandouts__courseID_courseID(Parameter) case m_getUpdates__courseID_courseID(Parameter) case m_resumeBlock__courseID_courseID(Parameter) - case m_getSubtitles__url_url(Parameter) + case m_getSubtitles__url_urlselectedLanguage_selectedLanguage(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1275,9 +1277,10 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) return Matcher.ComparisonResult(results) - case (.m_getSubtitles__url_url(let lhsUrl), .m_getSubtitles__url_url(let rhsUrl)): + case (.m_getSubtitles__url_urlselectedLanguage_selectedLanguage(let lhsUrl, let lhsSelectedlanguage), .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(let rhsUrl, let rhsSelectedlanguage)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSelectedlanguage, rhs: rhsSelectedlanguage, with: matcher), lhsSelectedlanguage, rhsSelectedlanguage, "selectedLanguage")) return Matcher.ComparisonResult(results) default: return .none } @@ -1295,7 +1298,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { case let .m_getHandouts__courseID_courseID(p0): return p0.intValue case let .m_getUpdates__courseID_courseID(p0): return p0.intValue case let .m_resumeBlock__courseID_courseID(p0): return p0.intValue - case let .m_getSubtitles__url_url(p0): return p0.intValue + case let .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { @@ -1310,7 +1313,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { case .m_getHandouts__courseID_courseID: return ".getHandouts(courseID:)" case .m_getUpdates__courseID_courseID: return ".getUpdates(courseID:)" case .m_resumeBlock__courseID_courseID: return ".resumeBlock(courseID:)" - case .m_getSubtitles__url_url: return ".getSubtitles(url:)" + case .m_getSubtitles__url_urlselectedLanguage_selectedLanguage: return ".getSubtitles(url:selectedLanguage:)" } } } @@ -1351,8 +1354,8 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { public static func resumeBlock(courseID: Parameter, willReturn: ResumeBlock...) -> MethodStub { return Given(method: .m_resumeBlock__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getSubtitles(url: Parameter, willReturn: [Subtitle]...) -> MethodStub { - return Given(method: .m_getSubtitles__url_url(`url`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func getSubtitles(url: Parameter, selectedLanguage: Parameter, willReturn: [Subtitle]...) -> MethodStub { + return Given(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getCourseVideoBlocks(fullStructure: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { let willReturn: [CourseStructure] = [] @@ -1451,12 +1454,12 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { willProduce(stubber) return given } - public static func getSubtitles(url: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_getSubtitles__url_url(`url`), products: willThrow.map({ StubProduct.throw($0) })) + public static func getSubtitles(url: Parameter, selectedLanguage: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getSubtitles(url: Parameter, willProduce: (StubberThrows<[Subtitle]>) -> Void) -> MethodStub { + public static func getSubtitles(url: Parameter, selectedLanguage: Parameter, willProduce: (StubberThrows<[Subtitle]>) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getSubtitles__url_url(`url`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: ([Subtitle]).self) willProduce(stubber) return given @@ -1476,7 +1479,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { public static func getHandouts(courseID: Parameter) -> Verify { return Verify(method: .m_getHandouts__courseID_courseID(`courseID`))} public static func getUpdates(courseID: Parameter) -> Verify { return Verify(method: .m_getUpdates__courseID_courseID(`courseID`))} public static func resumeBlock(courseID: Parameter) -> Verify { return Verify(method: .m_resumeBlock__courseID_courseID(`courseID`))} - public static func getSubtitles(url: Parameter) -> Verify { return Verify(method: .m_getSubtitles__url_url(`url`))} + public static func getSubtitles(url: Parameter, selectedLanguage: Parameter) -> Verify { return Verify(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`))} } public struct Perform { @@ -1513,8 +1516,8 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { public static func resumeBlock(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resumeBlock__courseID_courseID(`courseID`), performs: perform) } - public static func getSubtitles(url: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getSubtitles__url_url(`url`), performs: perform) + public static func getSubtitles(url: Parameter, selectedLanguage: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`), performs: perform) } } diff --git a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift index 49969ccb3..cea2c631b 100644 --- a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift @@ -16,6 +16,7 @@ final class CourseContainerViewModelTests: XCTestCase { func testGetCourseBlocksSuccess() async throws { let interactor = CourseInteractorProtocolMock() + let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() let config = ConfigMock() let connectivity = ConnectivityProtocolMock() @@ -24,6 +25,7 @@ final class CourseContainerViewModelTests: XCTestCase { let viewModel = CourseContainerViewModel( interactor: interactor, + authInteractor: authInteractor, router: router, config: config, connectivity: connectivity, @@ -112,6 +114,7 @@ final class CourseContainerViewModelTests: XCTestCase { func testGetCourseBlocksOfflineSuccess() async throws { let interactor = CourseInteractorProtocolMock() + let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() let config = ConfigMock() let connectivity = ConnectivityProtocolMock() @@ -120,6 +123,7 @@ final class CourseContainerViewModelTests: XCTestCase { let viewModel = CourseContainerViewModel( interactor: interactor, + authInteractor: authInteractor, router: router, config: config, connectivity: connectivity, @@ -160,6 +164,7 @@ final class CourseContainerViewModelTests: XCTestCase { func testGetCourseBlocksNoInternetError() async throws { let interactor = CourseInteractorProtocolMock() + let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() let config = ConfigMock() let connectivity = ConnectivityProtocolMock() @@ -168,6 +173,7 @@ final class CourseContainerViewModelTests: XCTestCase { let viewModel = CourseContainerViewModel( interactor: interactor, + authInteractor: authInteractor, router: router, config: config, connectivity: connectivity, @@ -196,6 +202,7 @@ final class CourseContainerViewModelTests: XCTestCase { func testGetCourseBlocksNoCacheError() async throws { let interactor = CourseInteractorProtocolMock() + let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() let config = ConfigMock() let connectivity = ConnectivityProtocolMock() @@ -204,6 +211,7 @@ final class CourseContainerViewModelTests: XCTestCase { let viewModel = CourseContainerViewModel( interactor: interactor, + authInteractor: authInteractor, router: router, config: config, connectivity: connectivity, @@ -229,6 +237,7 @@ final class CourseContainerViewModelTests: XCTestCase { func testGetCourseBlocksUnknownError() async throws { let interactor = CourseInteractorProtocolMock() + let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() let config = ConfigMock() let connectivity = ConnectivityProtocolMock() @@ -237,6 +246,7 @@ final class CourseContainerViewModelTests: XCTestCase { let viewModel = CourseContainerViewModel( interactor: interactor, + authInteractor: authInteractor, router: router, config: config, connectivity: connectivity, diff --git a/Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift b/Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift index d2cf1835e..261a34bae 100644 --- a/Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift +++ b/Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift @@ -29,33 +29,6 @@ final class CourseDetailsViewModelTests: XCTestCase { cssInjector: cssInjector, connectivity: connectivity) - let items = [ - CourseItem(name: "Test", - org: "org", - shortDescription: "", - imageURL: "", - isActive: true, - courseStart: Date(), - courseEnd: nil, - enrollmentStart: Date(), - enrollmentEnd: Date(), - courseID: "123", - numPages: 2, - coursesCount: 2), - CourseItem(name: "Test2", - org: "org2", - shortDescription: "", - imageURL: "", - isActive: true, - courseStart: Date(), - courseEnd: nil, - enrollmentStart: Date(), - enrollmentEnd: Date(), - courseID: "1243", - numPages: 1, - coursesCount: 2) - ] - let courseDetails = CourseDetails( courseID: "123", org: "org", @@ -67,7 +40,8 @@ final class CourseDetailsViewModelTests: XCTestCase { enrollmentEnd: nil, isEnrolled: true, overviewHTML: "", - courseBannerURL: "" + courseBannerURL: "", + courseVideoURL: nil ) @@ -98,33 +72,6 @@ final class CourseDetailsViewModelTests: XCTestCase { cssInjector: cssInjector, connectivity: connectivity) - let items = [ - CourseItem(name: "Test", - org: "org", - shortDescription: "", - imageURL: "", - isActive: true, - courseStart: Date(), - courseEnd: nil, - enrollmentStart: Date(), - enrollmentEnd: Date(), - courseID: "123", - numPages: 2, - coursesCount: 2), - CourseItem(name: "Test2", - org: "org2", - shortDescription: "", - imageURL: "", - isActive: true, - courseStart: Date(), - courseEnd: nil, - enrollmentStart: Date(), - enrollmentEnd: Date(), - courseID: "1243", - numPages: 1, - coursesCount: 2) - ] - let courseDetails = CourseDetails( courseID: "123", org: "org", @@ -136,7 +83,8 @@ final class CourseDetailsViewModelTests: XCTestCase { enrollmentEnd: nil, isEnrolled: true, overviewHTML: "", - courseBannerURL: "" + courseBannerURL: "", + courseVideoURL: nil ) Given(interactor, .getCourseDetailsOffline(courseID: "123", diff --git a/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift index c5b39f7c9..2c01ac7e9 100644 --- a/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift @@ -14,24 +14,14 @@ import SwiftUI final class CourseUnitViewModelTests: XCTestCase { - let blocks = [ + static let blocks = [ CourseBlock(blockId: "1", id: "1", topicId: "1", graded: false, completion: 0, - type: .course, - displayName: "One", - studentUrl: "", - videoUrl: nil, - youTubeUrl: nil), - CourseBlock(blockId: "1", - id: "1", - topicId: "1", - graded: false, - completion: 0, - type: .course, - displayName: "One", + type: .video, + displayName: "Lesson 1", studentUrl: "", videoUrl: nil, youTubeUrl: nil), @@ -40,9 +30,9 @@ final class CourseUnitViewModelTests: XCTestCase { topicId: "2", graded: false, completion: 0, - type: .html, - displayName: "Two", - studentUrl: "", + type: .video, + displayName: "Lesson 2", + studentUrl: "2", videoUrl: nil, youTubeUrl: nil), CourseBlock(blockId: "3", @@ -50,9 +40,9 @@ final class CourseUnitViewModelTests: XCTestCase { topicId: "3", graded: false, completion: 0, - type: .discussion, - displayName: "Three", - studentUrl: "", + type: .unknown, + displayName: "Lesson 3", + studentUrl: "3", videoUrl: nil, youTubeUrl: nil), CourseBlock(blockId: "4", @@ -60,57 +50,72 @@ final class CourseUnitViewModelTests: XCTestCase { topicId: "4", graded: false, completion: 0, - type: .video, - displayName: "Four", - studentUrl: "", - videoUrl: "url", - youTubeUrl: "url"), - CourseBlock(blockId: "5", - id: "5", - topicId: "5", - graded: false, - completion: 0, - type: .video, - displayName: "Five", - studentUrl: "", - videoUrl: "url", - youTubeUrl: nil), - CourseBlock(blockId: "6", - id: "6", - topicId: "6", - graded: false, - completion: 0, - type: .video, - displayName: "Six", - studentUrl: "", + type: .unknown, + displayName: "4", + studentUrl: "4", videoUrl: nil, youTubeUrl: nil), - CourseBlock(blockId: "7", - id: "7", - topicId: "7", - graded: false, - completion: 0, - type: .problem, - displayName: "Seven", - studentUrl: "", - videoUrl: nil, - youTubeUrl: nil) ] + let chapters = [ + CourseChapter( + blockId: "0", + id: "0", + displayName: "0", + type: .chapter, + childs: [ + CourseSequential(blockId: "5", + id: "5", + displayName: "5", + type: .sequential, + completion: 0, + childs: [ + CourseVertical(blockId: "6", id: "6", + displayName: "6", + type: .vertical, + completion: 0, + childs: blocks) + ]) + + ]), + CourseChapter( + blockId: "2", + id: "2", + displayName: "2", + type: .chapter, + childs: [ + CourseSequential(blockId: "3", + id: "3", + displayName: "3", + type: .sequential, + completion: 0, + childs: [ + CourseVertical(blockId: "4", id: "4", + displayName: "4", + type: .vertical, + completion: 0, + childs: blocks) + ]) + + ]) + ] + func testBlockCompletionRequestSuccess() async throws { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = CourseUnitViewModel( - lessonID: "123", - courseID: "456", - blocks: blocks, - interactor: interactor, - router: router, - connectivity: connectivity, - manager: DownloadManagerMock() - ) + let viewModel = CourseUnitViewModel(lessonID: "123", + courseID: "456", + id: "789", + chapters: chapters, + chapterIndex: 0, + sequentialIndex: 0, + verticalIndex: 0, + interactor: interactor, + router: router, + connectivity: connectivity, + manager: DownloadManagerMock()) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willProduce: {_ in})) @@ -124,15 +129,17 @@ final class CourseUnitViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = CourseUnitViewModel( - lessonID: "123", - courseID: "456", - blocks: blocks, - interactor: interactor, - router: router, - connectivity: connectivity, - manager: DownloadManagerMock() - ) + let viewModel = CourseUnitViewModel(lessonID: "123", + courseID: "456", + id: "789", + chapters: chapters, + chapterIndex: 0, + sequentialIndex: 0, + verticalIndex: 0, + interactor: interactor, + router: router, + connectivity: connectivity, + manager: DownloadManagerMock()) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, @@ -151,15 +158,17 @@ final class CourseUnitViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = CourseUnitViewModel( - lessonID: "123", - courseID: "456", - blocks: blocks, - interactor: interactor, - router: router, - connectivity: connectivity, - manager: DownloadManagerMock() - ) + let viewModel = CourseUnitViewModel(lessonID: "123", + courseID: "456", + id: "789", + chapters: chapters, + chapterIndex: 0, + sequentialIndex: 0, + verticalIndex: 0, + interactor: interactor, + router: router, + connectivity: connectivity, + manager: DownloadManagerMock()) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -180,15 +189,17 @@ final class CourseUnitViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = CourseUnitViewModel( - lessonID: "123", - courseID: "456", - blocks: blocks, - interactor: interactor, - router: router, - connectivity: connectivity, - manager: DownloadManagerMock() - ) + let viewModel = CourseUnitViewModel(lessonID: "123", + courseID: "456", + id: "789", + chapters: chapters, + chapterIndex: 0, + sequentialIndex: 0, + verticalIndex: 0, + interactor: interactor, + router: router, + connectivity: connectivity, + manager: DownloadManagerMock()) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, @@ -208,28 +219,28 @@ final class CourseUnitViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = CourseUnitViewModel( - lessonID: "123", - courseID: "456", - blocks: blocks, - interactor: interactor, - router: router, - connectivity: connectivity, - manager: DownloadManagerMock() - ) + let viewModel = CourseUnitViewModel(lessonID: "123", + courseID: "456", + id: "789", + chapters: chapters, + chapterIndex: 0, + sequentialIndex: 0, + verticalIndex: 0, + interactor: interactor, + router: router, + connectivity: connectivity, + manager: DownloadManagerMock()) viewModel.loadIndex() - for _ in 0...blocks.count - 1 { + for _ in 0...CourseUnitViewModelTests.blocks.count - 1 { viewModel.select(move: .next) - viewModel.createLessonType() } - XCTAssertEqual(viewModel.index, 7) + XCTAssertEqual(viewModel.index, 3) - for _ in 0...blocks.count - 1 { + for _ in 0...CourseUnitViewModelTests.blocks.count - 1 { viewModel.select(move: .previous) - viewModel.createLessonType() } diff --git a/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift b/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift index 763a5c420..83295daf7 100644 --- a/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift @@ -31,15 +31,18 @@ final class VideoPlayerViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - Given(interactor, .getSubtitles(url: .any, willReturn: subtitles)) + Given(interactor, .getSubtitles(url: .any, selectedLanguage: .any, willReturn: subtitles)) - let viewModel = VideoPlayerViewModel(interactor: interactor, + let viewModel = VideoPlayerViewModel(blockID: "", + courseID: "", + languages: [], + interactor: interactor, router: router, connectivity: connectivity) await viewModel.getSubtitles(subtitlesUrl: "url") - Verify(interactor, .getSubtitles(url: .any)) + Verify(interactor, .getSubtitles(url: .any, selectedLanguage: .any)) XCTAssertEqual(viewModel.subtitles.first!.text, subtitles.first!.text) XCTAssertNil(viewModel.errorMessage) @@ -54,15 +57,18 @@ final class VideoPlayerViewModelTests: XCTestCase { Given(connectivity, .isInternetAvaliable(getter: false)) - Given(interactor, .getSubtitles(url: .any, willReturn: subtitles)) + Given(interactor, .getSubtitles(url: .any, selectedLanguage: .any, willReturn: subtitles)) - let viewModel = VideoPlayerViewModel(interactor: interactor, + let viewModel = VideoPlayerViewModel(blockID: "", + courseID: "", + languages: [], + interactor: interactor, router: router, connectivity: connectivity) await viewModel.getSubtitles(subtitlesUrl: "url") - Verify(interactor, .getSubtitles(url: .any)) + Verify(interactor, .getSubtitles(url: .any, selectedLanguage: .any)) XCTAssertEqual(viewModel.subtitles.first!.text, subtitles.first!.text) XCTAssertNil(viewModel.errorMessage) @@ -74,7 +80,10 @@ final class VideoPlayerViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = VideoPlayerViewModel(interactor: interactor, + let viewModel = VideoPlayerViewModel(blockID: "", + courseID: "", + languages: [], + interactor: interactor, router: router, connectivity: connectivity) @@ -83,7 +92,7 @@ final class VideoPlayerViewModelTests: XCTestCase { SubtitleUrl(language: "uk", url: "url2") ] - Given(interactor, .getSubtitles(url: .any, willReturn: subtitles)) + Given(interactor, .getSubtitles(url: .any, selectedLanguage: .any, willReturn: subtitles)) await viewModel.getSubtitles(subtitlesUrl: "url") viewModel.prepareLanguages() @@ -98,13 +107,16 @@ final class VideoPlayerViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = VideoPlayerViewModel(interactor: interactor, + let viewModel = VideoPlayerViewModel(blockID: "", + courseID: "", + languages: [], + interactor: interactor, router: router, connectivity: connectivity) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willProduce: {_ in})) - await viewModel.blockCompletionRequest(blockID: "123", courseID: "123") + await viewModel.blockCompletionRequest() Verify(interactor, .blockCompletionRequest(courseID: .any, blockID: .any)) } @@ -114,13 +126,16 @@ final class VideoPlayerViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = VideoPlayerViewModel(interactor: interactor, + let viewModel = VideoPlayerViewModel(blockID: "", + courseID: "", + languages: [], + interactor: interactor, router: router, connectivity: connectivity) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: NSError())) - await viewModel.blockCompletionRequest(blockID: "123", courseID: "123") + await viewModel.blockCompletionRequest() Verify(interactor, .blockCompletionRequest(courseID: .any, blockID: .any)) @@ -135,13 +150,16 @@ final class VideoPlayerViewModelTests: XCTestCase { let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) - let viewModel = VideoPlayerViewModel(interactor: interactor, + let viewModel = VideoPlayerViewModel(blockID: "", + courseID: "", + languages: [], + interactor: interactor, router: router, connectivity: connectivity) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: noInternetError)) - await viewModel.blockCompletionRequest(blockID: "123", courseID: "123") + await viewModel.blockCompletionRequest() Verify(interactor, .blockCompletionRequest(courseID: .any, blockID: .any)) diff --git a/Dashboard/Dashboard.xcodeproj/project.pbxproj b/Dashboard/Dashboard.xcodeproj/project.pbxproj index f177c68a8..6d116fcf2 100644 --- a/Dashboard/Dashboard.xcodeproj/project.pbxproj +++ b/Dashboard/Dashboard.xcodeproj/project.pbxproj @@ -482,7 +482,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -503,7 +503,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -524,7 +524,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -545,7 +545,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -566,7 +566,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -587,7 +587,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -707,7 +707,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -820,7 +820,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; diff --git a/Dashboard/Dashboard/Presentation/DashboardView.swift b/Dashboard/Dashboard/Presentation/DashboardView.swift index 78e3c802a..a47dca8e3 100644 --- a/Dashboard/Dashboard/Presentation/DashboardView.swift +++ b/Dashboard/Dashboard/Presentation/DashboardView.swift @@ -39,7 +39,6 @@ public struct DashboardView: View { ZStack { Text(DashboardLocalization.title) .titleSettings() - } ZStack { diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index d8e9e8131..548809f83 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -514,10 +514,10 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`) } - open func presentAlert(alertTitle: String, alertMessage: String, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void) { - addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) - let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) as? (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void - perform?(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`) + open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void + perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`) } open func presentView(transitionStyle: UIModalTransitionStyle, view: any View) { @@ -544,7 +544,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showRegisterScreen case m_showForgotPasswordScreen case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) - case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) case m_presentView__transitionStyle_transitionStylecontent_content(Parameter, Parameter<() -> any View>) @@ -590,14 +590,16 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) return Matcher.ComparisonResult(results) - case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped)): + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsNextsectionname, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped, let lhsNextsectiontapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsNextsectionname, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped, let rhsNextsectiontapped)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectionname, rhs: rhsNextsectionname, with: matcher), lhsNextsectionname, rhsNextsectionname, "nextSectionName")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsImage, rhs: rhsImage, with: matcher), lhsImage, rhsImage, "image")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectiontapped, rhs: rhsNextsectiontapped, with: matcher), lhsNextsectiontapped, rhsNextsectiontapped, "nextSectionTapped")) return Matcher.ComparisonResult(results) case (.m_presentView__transitionStyle_transitionStyleview_view(let lhsTransitionstyle, let lhsView), .m_presentView__transitionStyle_transitionStyleview_view(let rhsTransitionstyle, let rhsView)): @@ -627,7 +629,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return 0 case .m_showForgotPasswordScreen: return 0 case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue - case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue case let .m_presentView__transitionStyle_transitionStylecontent_content(p0, p1): return p0.intValue + p1.intValue } @@ -644,7 +646,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return ".showRegisterScreen()" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" - case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped: return ".presentAlert(alertTitle:alertMessage:action:image:onCloseTapped:okTapped:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" case .m_presentView__transitionStyle_transitionStylecontent_content: return ".presentView(transitionStyle:content:)" } @@ -675,7 +677,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} public static func presentView(transitionStyle: Parameter, content: Parameter<() -> any View>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStylecontent_content(`transitionStyle`, `content`))} } @@ -714,8 +716,8 @@ open class BaseRouterMock: BaseRouter, Mock { public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, perform: @escaping (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { - return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`), performs: perform) + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>, perform: @escaping (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`), performs: perform) } public static func presentView(transitionStyle: Parameter, view: Parameter, perform: @escaping (UIModalTransitionStyle, any View) -> Void) -> Perform { return Perform(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`), performs: perform) diff --git a/Discovery/Discovery.xcodeproj/project.pbxproj b/Discovery/Discovery.xcodeproj/project.pbxproj index 3099a4e0f..c4cf2bbd2 100644 --- a/Discovery/Discovery.xcodeproj/project.pbxproj +++ b/Discovery/Discovery.xcodeproj/project.pbxproj @@ -508,7 +508,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -529,7 +529,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -550,7 +550,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -571,7 +571,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -592,7 +592,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -613,7 +613,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -733,7 +733,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -846,7 +846,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; diff --git a/Discovery/Discovery/Presentation/DiscoveryView.swift b/Discovery/Discovery/Presentation/DiscoveryView.swift index 492493898..8b5af1c7a 100644 --- a/Discovery/Discovery/Presentation/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/DiscoveryView.swift @@ -22,7 +22,7 @@ public struct DiscoveryView: View { Text(DiscoveryLocalization.Header.title2) .font(Theme.Fonts.titleSmall) .foregroundColor(CoreAssets.textPrimary.swiftUIColor) - }.listRowBackground(Color.clear) + }.listRowBackground(Color.clear) public init(viewModel: DiscoveryViewModel, router: DiscoveryRouter) { self.viewModel = viewModel @@ -38,10 +38,8 @@ public struct DiscoveryView: View { // MARK: - Page name VStack(alignment: .center) { ZStack { - Text(DiscoveryLocalization.title) .titleSettings(top: 10) - } // MARK: - Search fake field @@ -92,18 +90,18 @@ public struct DiscoveryView: View { type: .discovery, index: index, cellsCount: viewModel.courses.count) - .padding(.horizontal, 24) - .onAppear { - Task { - await viewModel.getDiscoveryCourses(index: index) - } - } - .onTapGesture { - router.showCourseDetais( - courseID: course.courseID, - title: course.name - ) + .padding(.horizontal, 24) + .onAppear { + Task { + await viewModel.getDiscoveryCourses(index: index) } + } + .onTapGesture { + router.showCourseDetais( + courseID: course.courseID, + title: course.name + ) + } } // MARK: - ProgressBar diff --git a/Discovery/Discovery/Presentation/SearchView.swift b/Discovery/Discovery/Presentation/SearchView.swift index 2af2557ba..aecd40f5a 100644 --- a/Discovery/Discovery/Presentation/SearchView.swift +++ b/Discovery/Discovery/Presentation/SearchView.swift @@ -104,7 +104,10 @@ public struct SearchView: View { .padding(.horizontal, 24) .onAppear { Task { - await viewModel.searchCourses(index: index, searchTerm: viewModel.searchText) + await viewModel.searchCourses( + index: index, + searchTerm: viewModel.searchText + ) } } .onTapGesture { diff --git a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift index 77f59e84b..c76e3205c 100644 --- a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift +++ b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift @@ -514,10 +514,10 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`) } - open func presentAlert(alertTitle: String, alertMessage: String, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void) { - addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) - let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) as? (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void - perform?(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`) + open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void + perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`) } open func presentView(transitionStyle: UIModalTransitionStyle, view: any View) { @@ -544,7 +544,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showRegisterScreen case m_showForgotPasswordScreen case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) - case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) case m_presentView__transitionStyle_transitionStylecontent_content(Parameter, Parameter<() -> any View>) @@ -590,14 +590,16 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) return Matcher.ComparisonResult(results) - case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped)): + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsNextsectionname, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped, let lhsNextsectiontapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsNextsectionname, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped, let rhsNextsectiontapped)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectionname, rhs: rhsNextsectionname, with: matcher), lhsNextsectionname, rhsNextsectionname, "nextSectionName")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsImage, rhs: rhsImage, with: matcher), lhsImage, rhsImage, "image")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectiontapped, rhs: rhsNextsectiontapped, with: matcher), lhsNextsectiontapped, rhsNextsectiontapped, "nextSectionTapped")) return Matcher.ComparisonResult(results) case (.m_presentView__transitionStyle_transitionStyleview_view(let lhsTransitionstyle, let lhsView), .m_presentView__transitionStyle_transitionStyleview_view(let rhsTransitionstyle, let rhsView)): @@ -627,7 +629,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return 0 case .m_showForgotPasswordScreen: return 0 case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue - case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue case let .m_presentView__transitionStyle_transitionStylecontent_content(p0, p1): return p0.intValue + p1.intValue } @@ -644,7 +646,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return ".showRegisterScreen()" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" - case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped: return ".presentAlert(alertTitle:alertMessage:action:image:onCloseTapped:okTapped:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" case .m_presentView__transitionStyle_transitionStylecontent_content: return ".presentView(transitionStyle:content:)" } @@ -675,7 +677,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} public static func presentView(transitionStyle: Parameter, content: Parameter<() -> any View>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStylecontent_content(`transitionStyle`, `content`))} } @@ -714,8 +716,8 @@ open class BaseRouterMock: BaseRouter, Mock { public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, perform: @escaping (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { - return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`), performs: perform) + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>, perform: @escaping (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`), performs: perform) } public static func presentView(transitionStyle: Parameter, view: Parameter, perform: @escaping (UIModalTransitionStyle, any View) -> Void) -> Perform { return Perform(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`), performs: perform) diff --git a/Discussion/Discussion.xcodeproj/project.pbxproj b/Discussion/Discussion.xcodeproj/project.pbxproj index ad6feb624..f8a61da82 100644 --- a/Discussion/Discussion.xcodeproj/project.pbxproj +++ b/Discussion/Discussion.xcodeproj/project.pbxproj @@ -1315,7 +1315,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -1336,7 +1336,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -1357,7 +1357,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -1378,7 +1378,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -1399,7 +1399,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -1420,7 +1420,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -1539,7 +1539,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; @@ -1651,7 +1651,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; diff --git a/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift b/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift index d95cde415..37649c682 100644 --- a/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift +++ b/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift @@ -73,10 +73,29 @@ public extension DataLayer { case users } - public init(id: String, author: String?, authorLabel: String?, createdAt: String, updatedAt: String, rawBody: String, - renderedBody: String, abuseFlagged: Bool, voted: Bool, voteCount: Int, editableFields: [String], - canDelete: Bool, threadID: String, parentID: String?, endorsed: Bool, endorsedBy: String?, - endorsedByLabel: String?, endorsedAt: String?, childCount: Int, children: [String], users: Users?) { + public init( + id: String, + author: String?, + authorLabel: String?, + createdAt: String, + updatedAt: String, + rawBody: String, + renderedBody: String, + abuseFlagged: Bool, + voted: Bool, + voteCount: Int, + editableFields: [String], + canDelete: Bool, + threadID: String, + parentID: String?, + endorsed: Bool, + endorsedBy: String?, + endorsedByLabel: String?, + endorsedAt: String?, + childCount: Int, + children: [String], + users: Users? + ) { self.id = id self.author = author self.authorLabel = authorLabel diff --git a/Discussion/Discussion/Data/Model/Data_CreatedComment.swift b/Discussion/Discussion/Data/Model/Data_CreatedComment.swift index 84b7bdc92..e94cadb31 100644 --- a/Discussion/Discussion/Data/Model/Data_CreatedComment.swift +++ b/Discussion/Discussion/Data/Model/Data_CreatedComment.swift @@ -61,7 +61,9 @@ public extension DataLayer { public extension DataLayer.CreatedComment { var domain: Post { Post(authorName: author ?? DiscussionLocalization.anonymous, - authorAvatar: profileImage.imageURLSmall?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "", + authorAvatar: profileImage.imageURLSmall?.addingPercentEncoding( + withAllowedCharacters: .urlHostAllowed + ) ?? "", postDate: Date(iso8601: createdAt), postTitle: "", postBodyHtml: renderedBody, diff --git a/Discussion/Discussion/Data/Network/DiscussionRepository.swift b/Discussion/Discussion/Data/Network/DiscussionRepository.swift index d213ddb67..c10bb0e7b 100644 --- a/Discussion/Discussion/Data/Network/DiscussionRepository.swift +++ b/Discussion/Discussion/Data/Network/DiscussionRepository.swift @@ -132,7 +132,9 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { if let stringJSON = String(data: data, encoding: .utf8) { modifiedJSON = renameUsersInJSON(stringJSON: stringJSON) - if let modifiedParsed = try modifiedJSON.data(using: .utf8)?.mapResponse(DataLayer.ThreadListsResponse.self) { + if let modifiedParsed = try modifiedJSON.data(using: .utf8)?.mapResponse( + DataLayer.ThreadListsResponse.self + ) { return modifiedParsed } else { return parsed diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift index 7f8dde1b0..1a410b953 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift @@ -39,143 +39,149 @@ public struct ResponsesView: View { ZStack(alignment: .top) { // MARK: - Page name - VStack(alignment: .center) { - NavigationBar(title: title, - leftButtonAction: { router.back() }) - - // MARK: - Page Body - ScrollViewReader { scroll in - VStack { - ZStack(alignment: .top) { - RefreshableScrollViewCompat(action: { - viewModel.comments = [] - _ = await viewModel.getComments(commentID: commentID, - parentComment: parentComment, page: 1) - }) { - VStack { - if let comments = viewModel.postComments { - ParentCommentView( - comments: comments, - isThread: false, + VStack(alignment: .center) { + NavigationBar(title: title, + leftButtonAction: { router.back() }) + + // MARK: - Page Body + ScrollViewReader { scroll in + VStack { + ZStack(alignment: .top) { + RefreshableScrollViewCompat(action: { + viewModel.comments = [] + _ = await viewModel.getComments(commentID: commentID, + parentComment: parentComment, page: 1) + }) { + VStack { + if let comments = viewModel.postComments { + ParentCommentView( + comments: comments, + isThread: false, + onLikeTap: { + Task { + if await viewModel.vote( + id: parentComment.commentID, + isThread: false, + voted: comments.voted, + index: nil + ) { + viewModel.sendThreadLikeState() + } + } + }, + onReportTap: { + Task { + if await viewModel.flag( + id: parentComment.commentID, + isThread: false, + abuseFlagged: comments.abuseFlagged, + index: nil + ) { + viewModel.sendThreadReportState() + } + + } + }, + onFollowTap: {} + ) + HStack { + Text("\(viewModel.itemsCount)") + Text(DiscussionLocalization.commentsCount(viewModel.itemsCount)) + Spacer() + }.padding(.top, 40) + .padding(.bottom, 14) + .padding(.leading, 24) + .font(Theme.Fonts.titleMedium) + ForEach( + Array(comments.comments.enumerated()), id: \.offset + ) { index, comment in + CommentCell( + comment: comment, + addCommentAvailable: false, leftLineEnabled: true, onLikeTap: { Task { - if await viewModel.vote( - id: parentComment.commentID, + await viewModel.vote( + id: comment.commentID, isThread: false, - voted: comments.voted, - index: nil - ) { - viewModel.sendThreadLikeState() - } + voted: comment.voted, + index: index + ) } }, onReportTap: { Task { - if await viewModel.flag( - id: parentComment.commentID, + await viewModel.flag( + id: comment.commentID, isThread: false, - abuseFlagged: comments.abuseFlagged, - index: nil - ) { - viewModel.sendThreadReportState() - } - + abuseFlagged: comment.abuseFlagged, + index: index + ) } }, - onFollowTap: {} - ) - HStack { - Text("\(viewModel.itemsCount)") - Text(DiscussionLocalization.commentsCount(viewModel.itemsCount)) - Spacer() - }.padding(.top, 40) - .padding(.bottom, 14) - .padding(.leading, 24) - .font(Theme.Fonts.titleMedium) - ForEach(Array(comments.comments.enumerated()), id: \.offset) { index, comment in - CommentCell( - comment: comment, - addCommentAvailable: false, leftLineEnabled: true, - onLikeTap: { - Task { - await viewModel.vote( - id: comment.commentID, - isThread: false, - voted: comment.voted, - index: index - ) - } - }, - onReportTap: { - Task { - await viewModel.flag( - id: comment.commentID, - isThread: false, - abuseFlagged: comment.abuseFlagged, - index: index - ) - } - }, - onCommentsTap: {}, - onFetchMore: { - Task { - await viewModel.fetchMorePosts(commentID: commentID, - parentComment: parentComment, - index: index) - } + onCommentsTap: {}, + onFetchMore: { + Task { + await viewModel.fetchMorePosts( + commentID: commentID, + parentComment: parentComment, + index: index + ) } - ) - .id(index) - .padding(.bottom, -8) - } - if viewModel.nextPage <= viewModel.totalPages { - VStack(alignment: .center) { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 20) - }.frame(maxWidth: .infinity, - maxHeight: .infinity) - } + } + ) + .id(index) + .padding(.bottom, -8) + } + if viewModel.nextPage <= viewModel.totalPages { + VStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 20) + }.frame(maxWidth: .infinity, + maxHeight: .infinity) } - Spacer(minLength: 84) - } - .onRightSwipeGesture { - viewModel.router.back() } - }.frameLimit() - - if !parentComment.closed { - FlexibleKeyboardInputView( - hint: DiscussionLocalization.Response.addComment, - sendText: { commentText in - if let threadID = viewModel.postComments?.threadID { - Task { - await viewModel.postComment(threadID: threadID, - rawBody: commentText, - parentID: viewModel.postComments?.parentID) - } + Spacer(minLength: 84) + } + .onRightSwipeGesture { + viewModel.router.back() + } + }.frameLimit() + + if !parentComment.closed { + FlexibleKeyboardInputView( + hint: DiscussionLocalization.Response.addComment, + sendText: { commentText in + if let threadID = viewModel.postComments?.threadID { + Task { + await viewModel.postComment( + threadID: threadID, + rawBody: commentText, + parentID: viewModel.postComments?.parentID + ) } } - ) - } + } + ) } } - .onReceive(viewModel.addPostSubject, perform: { newComment in - guard let newComment else { return } - viewModel.sendThreadPostsCountState() - if viewModel.nextPage - 1 == viewModel.totalPages { - viewModel.addNewPost(newComment) - withAnimation { - guard let count = viewModel.postComments?.comments.count else { return } - scroll.scrollTo(count - 2, anchor: .top) - } - } else { - viewModel.alertMessage = DiscussionLocalization.Response.Alert.commentAdded - viewModel.showAlert = true + } + .onReceive(viewModel.addPostSubject, perform: { newComment in + guard let newComment else { return } + viewModel.sendThreadPostsCountState() + if viewModel.nextPage - 1 == viewModel.totalPages { + viewModel.addNewPost(newComment) + withAnimation { + guard let count = viewModel.postComments?.comments.count else { return } + scroll.scrollTo(count - 2, anchor: .top) } - }) - .frame(maxWidth: .infinity, maxHeight: .infinity) - }.scrollAvoidKeyboard(dismissKeyboardByTap: true) - } + } else { + viewModel.alertMessage = DiscussionLocalization.Response.Alert.commentAdded + viewModel.showAlert = true + } + }) + .frame(maxWidth: .infinity, maxHeight: .infinity) + }.scrollAvoidKeyboard(dismissKeyboardByTap: true) + } // MARK: - Error Alert if viewModel.showError { VStack { @@ -228,19 +234,21 @@ struct ResponsesView_Previews: PreviewProvider { ) let router = DiscussionRouterMock() - ResponsesView(commentID: "", - viewModel: viewModel, - router: router, - parentComment: post + ResponsesView( + commentID: "", + viewModel: viewModel, + router: router, + parentComment: post ) .loadFonts() .preferredColorScheme(.light) .previewDisplayName("ResponsesView Light") - ResponsesView(commentID: "", - viewModel: viewModel, - router: router, - parentComment: post + ResponsesView( + commentID: "", + viewModel: viewModel, + router: router, + parentComment: post ) .loadFonts() .preferredColorScheme(.dark) diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift index b165565b9..1b8e0acc3 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift @@ -53,7 +53,7 @@ public class ResponsesViewModel: BaseResponsesViewModel, ObservableObject { } @MainActor - public func postComment(threadID: String, rawBody: String, parentID: String?) async { + func postComment(threadID: String, rawBody: String, parentID: String?) async { isShowProgress = true do { let newComment = try await interactor.addCommentTo(threadID: threadID, @@ -72,7 +72,7 @@ public class ResponsesViewModel: BaseResponsesViewModel, ObservableObject { } @MainActor - public func fetchMorePosts(commentID: String, parentComment: Post, index: Int) async { + func fetchMorePosts(commentID: String, parentComment: Post, index: Int) async { if totalPages > 1 { if index == comments.count - 3 { if totalPages != 1 { @@ -89,7 +89,7 @@ public class ResponsesViewModel: BaseResponsesViewModel, ObservableObject { } @MainActor - public func getComments(commentID: String, parentComment: Post, page: Int) async -> Bool { + func getComments(commentID: String, parentComment: Post, page: Int) async -> Bool { guard !fetchInProgress else { return false } do { let (comments, pagination) = try await interactor diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index 12513d490..38909a86d 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -169,9 +169,11 @@ public struct ThreadView: View { sendText: { commentText in if let threadID = viewModel.postComments?.threadID { Task { - await viewModel.postComment(threadID: threadID, - rawBody: commentText, - parentID: viewModel.postComments?.parentID) + await viewModel.postComment( + threadID: threadID, + rawBody: commentText, + parentID: viewModel.postComments?.parentID + ) } } } @@ -215,8 +217,10 @@ public struct ThreadView: View { if viewModel.showAlert { VStack { Text(viewModel.alertMessage ?? "") - .shadowCardStyle(bgColor: CoreAssets.accentColor.swiftUIColor, - textColor: .white) + .shadowCardStyle( + bgColor: CoreAssets.accentColor.swiftUIColor, + textColor: .white + ) .padding(.top, 80) Spacer() diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index aecb3202e..26d82ce7e 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -45,41 +45,45 @@ public class ThreadViewModel: BaseResponsesViewModel, ObservableObject { } func generateComments(comments: [UserComment], thread: UserThread) -> Post { - var result = Post(authorName: thread.author, - authorAvatar: thread.avatar, - postDate: thread.createdAt, - postTitle: thread.title, - postBodyHtml: thread.renderedBody, - postBody: thread.rawBody, - postVisible: true, - voted: thread.voted, - followed: thread.following, - votesCount: thread.voteCount, - responsesCount: comments.last?.responsesCount ?? 0, - comments: [], - threadID: thread.id, - commentID: thread.courseID, - parentID: nil, - abuseFlagged: thread.abuseFlagged, - closed: thread.closed) + var result = Post( + authorName: thread.author, + authorAvatar: thread.avatar, + postDate: thread.createdAt, + postTitle: thread.title, + postBodyHtml: thread.renderedBody, + postBody: thread.rawBody, + postVisible: true, + voted: thread.voted, + followed: thread.following, + votesCount: thread.voteCount, + responsesCount: comments.last?.responsesCount ?? 0, + comments: [], + threadID: thread.id, + commentID: thread.courseID, + parentID: nil, + abuseFlagged: thread.abuseFlagged, + closed: thread.closed + ) result.comments = comments.map { c in - Post(authorName: c.authorName, - authorAvatar: c.authorAvatar, - postDate: c.postDate, - postTitle: c.postTitle, - postBodyHtml: c.postBodyHtml, - postBody: c.postBody, - postVisible: c.postVisible, - voted: c.voted, - followed: c.followed, - votesCount: c.votesCount, - responsesCount: c.responsesCount, - comments: [], - threadID: c.threadID, - commentID: c.commentID, - parentID: c.parentID, - abuseFlagged: c.abuseFlagged, - closed: thread.closed) + Post( + authorName: c.authorName, + authorAvatar: c.authorAvatar, + postDate: c.postDate, + postTitle: c.postTitle, + postBodyHtml: c.postBodyHtml, + postBody: c.postBody, + postVisible: c.postVisible, + voted: c.voted, + followed: c.followed, + votesCount: c.votesCount, + responsesCount: c.responsesCount, + comments: [], + threadID: c.threadID, + commentID: c.commentID, + parentID: c.parentID, + abuseFlagged: c.abuseFlagged, + closed: thread.closed + ) } return result } diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift index 022656027..e21e8cf3c 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift @@ -23,10 +23,12 @@ public struct CreateNewThreadView: View { @ObservedObject private var viewModel: CreateNewThreadViewModel - public init(viewModel: CreateNewThreadViewModel, - selectedTopic: String, - courseID: String, - onPostCreated: @escaping () -> Void) { + public init( + viewModel: CreateNewThreadViewModel, + selectedTopic: String, + courseID: String, + onPostCreated: @escaping () -> Void + ) { self.viewModel = viewModel self.onPostCreated = onPostCreated self.courseID = courseID @@ -41,148 +43,150 @@ public struct CreateNewThreadView: View { public var body: some View { ZStack(alignment: .top) { - // MARK: - Page name - VStack(alignment: .center) { - NavigationBar(title: DiscussionLocalization.CreateThread.newPost, - leftButtonAction: { viewModel.router.back() }) - - // MARK: - Page Body - if viewModel.isShowProgress { - HStack(alignment: .center) { - ProgressBar(size: 40, lineWidth: 8) - .padding(20) - }.frame(maxWidth: .infinity, maxHeight: .infinity) - } else { - VStack(alignment: .leading) { - HStack { - Text(DiscussionLocalization.CreateThread.selectPostType) - .font(Theme.Fonts.titleMedium) - .foregroundColor(CoreAssets.textPrimary.swiftUIColor) - .padding(.top, 32) - Spacer() + // MARK: - Page name + VStack(alignment: .center) { + NavigationBar(title: DiscussionLocalization.CreateThread.newPost, + leftButtonAction: { viewModel.router.back() }) + + // MARK: - Page Body + if viewModel.isShowProgress { + HStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(20) + }.frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + ScrollView { + VStack(alignment: .leading) { + HStack { + Text(DiscussionLocalization.CreateThread.selectPostType) + .font(Theme.Fonts.titleMedium) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) + .padding(.top, 32) + Spacer() + } + + Picker("", selection: $postType) { + ForEach(postTypes, id: \.self) { + Text($0.localizedValue.capitalized) } + }.pickerStyle(.segmented) + .frame(maxWidth: .infinity, maxHeight: 40) + + // MARK: Topic picker + Group { + Text(DiscussionLocalization.CreateThread.topic) + .font(Theme.Fonts.titleSmall) + .padding(.top, 16) - Picker("", selection: $postType) { - ForEach(postTypes, id: \.self) { - Text($0.localizedValue.capitalized) - } - }.pickerStyle(.segmented) - .frame(maxWidth: .infinity, maxHeight: 40) - - // MARK: Topic picker - Group { - Text(DiscussionLocalization.CreateThread.topic) - .font(Theme.Fonts.titleSmall) - .padding(.top, 16) - - Menu { - Picker(selection: $viewModel.selectedTopic) { - ForEach(viewModel.allTopics, id: \.id) { - Text($0.name) - .tag($0.id) - .font(Theme.Fonts.labelLarge) - } - } label: {} - } label: { - HStack { - Text(viewModel.allTopics.first(where: { - $0.id == viewModel.selectedTopic })?.name ?? "") + Menu { + Picker(selection: $viewModel.selectedTopic) { + ForEach(viewModel.allTopics, id: \.id) { + Text($0.name) + .tag($0.id) .font(Theme.Fonts.labelLarge) - .frame(height: 40, alignment: .leading) - Spacer() - Image(systemName: "chevron.down") - }.padding(.horizontal, 14) - .accentColor(CoreAssets.textPrimary.swiftUIColor) - .background(Theme.Shapes.textInputShape - .fill(CoreAssets.textInputBackground.swiftUIColor) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill(CoreAssets.textInputStroke.swiftUIColor) - ) - } - } - // MARK: End of topic picker - - Group { - Text(DiscussionLocalization.CreateThread.title) - .font(Theme.Fonts.titleSmall) - + Text(" *").foregroundColor(.red) - }.padding(.top, 16) - TextField("", text: $postTitle) - .font(Theme.Fonts.labelLarge) - .padding(14) - .frame(height: 40) - .background( - Theme.Shapes.textInputShape - .fill(CoreAssets.textInputBackground.swiftUIColor) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill( - CoreAssets.textInputStroke.swiftUIColor - ) - ) - - Group { - Text("\(postType.localizedValue.capitalized)") - .font(Theme.Fonts.titleSmall) - + Text(" *").foregroundColor(.red) - }.padding(.top, 16) - TextEditor(text: $postBody) - .font(Theme.Fonts.labelLarge) - .padding(.horizontal, 10) - .padding(.vertical, 10) - .frame(height: 200) - .hideScrollContentBackground() - .background( - Theme.Shapes.textInputShape + } + } label: {} + } label: { + HStack { + Text(viewModel.allTopics.first(where: { + $0.id == viewModel.selectedTopic })?.name ?? "") + .font(Theme.Fonts.labelLarge) + .frame(height: 40, alignment: .leading) + Spacer() + Image(systemName: "chevron.down") + }.padding(.horizontal, 14) + .accentColor(CoreAssets.textPrimary.swiftUIColor) + .background(Theme.Shapes.textInputShape .fill(CoreAssets.textInputBackground.swiftUIColor) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill( - CoreAssets.textInputStroke.swiftUIColor - ) - ) - - CheckBoxView(checked: $followPost, - text: postType == .discussion - ? DiscussionLocalization.CreateThread.followDiscussion - : DiscussionLocalization.CreateThread.followQuestion + ) + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill(CoreAssets.textInputStroke.swiftUIColor) + ) + } + } + // MARK: End of topic picker + + Group { + Text(DiscussionLocalization.CreateThread.title) + .font(Theme.Fonts.titleSmall) + + Text(" *").foregroundColor(.red) + }.padding(.top, 16) + TextField("", text: $postTitle) + .font(Theme.Fonts.labelLarge) + .padding(14) + .frame(height: 40) + .background( + Theme.Shapes.textInputShape + .fill(CoreAssets.textInputBackground.swiftUIColor) ) - .padding(.top, 16) - - StyledButton(postType == .discussion - ? DiscussionLocalization.CreateThread.createDiscussion - : DiscussionLocalization.CreateThread.createQuestion, action: { - if postTitle != "" && postBody != "" { - let newThread = DiscussionNewThread(courseID: courseID, - topicID: viewModel.selectedTopic, - type: postType, - title: postTitle, - rawBody: postBody, - followPost: followPost) - Task { - if await viewModel.createNewThread(newThread: newThread) { - onPostCreated() - } + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill( + CoreAssets.textInputStroke.swiftUIColor + ) + ) + + Group { + Text("\(postType.localizedValue.capitalized)") + .font(Theme.Fonts.titleSmall) + + Text(" *").foregroundColor(.red) + }.padding(.top, 16) + TextEditor(text: $postBody) + .font(Theme.Fonts.labelLarge) + .padding(.horizontal, 10) + .padding(.vertical, 10) + .frame(height: 200) + .hideScrollContentBackground() + .background( + Theme.Shapes.textInputShape + .fill(CoreAssets.textInputBackground.swiftUIColor) + ) + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill( + CoreAssets.textInputStroke.swiftUIColor + ) + ) + + CheckBoxView(checked: $followPost, + text: postType == .discussion + ? DiscussionLocalization.CreateThread.followDiscussion + : DiscussionLocalization.CreateThread.followQuestion + ) + .padding(.top, 16) + + StyledButton(postType == .discussion + ? DiscussionLocalization.CreateThread.createDiscussion + : DiscussionLocalization.CreateThread.createQuestion, action: { + if postTitle != "" && postBody != "" { + let newThread = DiscussionNewThread(courseID: courseID, + topicID: viewModel.selectedTopic, + type: postType, + title: postTitle, + rawBody: postBody, + followPost: followPost) + Task { + if await viewModel.createNewThread(newThread: newThread) { + onPostCreated() } } - }) - .padding(.top, 26) - .saturation(!postTitle.isEmpty && !postBody.isEmpty ? 1 : 0) - Spacer() - }.padding(.horizontal, 24) - .frameLimit() - .onRightSwipeGesture { - viewModel.router.back() } - } + }) + .padding(.top, 26) + .saturation(!postTitle.isEmpty && !postBody.isEmpty ? 1 : 0) + Spacer() + }.padding(.horizontal, 24) + .frameLimit() + .onRightSwipeGesture { + viewModel.router.back() + } }.scrollAvoidKeyboard(dismissKeyboardByTap: true) + } + } } .background( CoreAssets.background.swiftUIColor diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift index a2f13a40c..95c907300 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift @@ -28,9 +28,11 @@ public class CreateNewThreadViewModel: ObservableObject { public let router: DiscussionRouter public let config: Config - public init(interactor: DiscussionInteractorProtocol, - router: DiscussionRouter, - config: Config) { + public init( + interactor: DiscussionInteractorProtocol, + router: DiscussionRouter, + config: Config + ) { self.interactor = interactor self.router = router self.config = config diff --git a/Discussion/Discussion/Presentation/DiscussionRouter.swift b/Discussion/Discussion/Presentation/DiscussionRouter.swift index 13be8b6c9..f57e883b9 100644 --- a/Discussion/Discussion/Presentation/DiscussionRouter.swift +++ b/Discussion/Discussion/Presentation/DiscussionRouter.swift @@ -21,7 +21,8 @@ public protocol DiscussionRouter: BaseRouter { func showComments( commentID: String, parentComment: Post, - threadStateSubject: CurrentValueSubject) + threadStateSubject: CurrentValueSubject + ) func createNewThread(courseID: String, selectedTopic: String, onPostCreated: @escaping () -> Void) } @@ -41,12 +42,13 @@ public class DiscussionRouterMock: BaseRouterMock, DiscussionRouter { public func showComments( commentID: String, parentComment: Post, - threadStateSubject: CurrentValueSubject) {} + threadStateSubject: CurrentValueSubject + ) {} public func createNewThread( courseID: String, selectedTopic: String, - onPostCreated: @escaping () -> Void) {} - + onPostCreated: @escaping () -> Void + ) {} } #endif diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift index dcee4ebef..a8025c28c 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift @@ -95,7 +95,7 @@ public class DiscussionSearchTopicsViewModel: ObservableObject { } @MainActor - public func searchCourses(index: Int, searchTerm: String) async { + func searchCourses(index: Int, searchTerm: String) async { if !fetchInProgress { if totalPages > 1 { if index == searchResults.count - 3 { diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift index 6e2f29f0f..ecbce5498 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift @@ -37,23 +37,27 @@ public class DiscussionTopicsViewModel: ObservableObject { func generateTopics(topics: Topics?) -> [DiscussionTopic] { var result = [ - DiscussionTopic(name: DiscussionLocalization.Topics.allPosts, - action: { - self.router.showThreads( - courseID: self.courseID, - topics: topics ?? Topics(coursewareTopics: [], nonCoursewareTopics: []), - title: DiscussionLocalization.Topics.allPosts, - type: .allPosts) - }, - style: .basic), - DiscussionTopic(name: DiscussionLocalization.Topics.postImFollowing, action: { - self.router.showThreads( - courseID: self.courseID, - topics: topics ?? Topics(coursewareTopics: [], nonCoursewareTopics: []), - title: DiscussionLocalization.Topics.postImFollowing, - type: .followingPosts - ) - }, style: .followed) + DiscussionTopic( + name: DiscussionLocalization.Topics.allPosts, + action: { + self.router.showThreads( + courseID: self.courseID, + topics: topics ?? Topics(coursewareTopics: [], nonCoursewareTopics: []), + title: DiscussionLocalization.Topics.allPosts, + type: .allPosts) + }, + style: .basic + ), + DiscussionTopic( + name: DiscussionLocalization.Topics.postImFollowing, action: { + self.router.showThreads( + courseID: self.courseID, + topics: topics ?? Topics(coursewareTopics: [], nonCoursewareTopics: []), + title: DiscussionLocalization.Topics.postImFollowing, + type: .followingPosts + ) + }, + style: .followed) ] if let topics = topics { for t in topics.nonCoursewareTopics { @@ -97,10 +101,14 @@ public class DiscussionTopicsViewModel: ObservableObject { result.append( DiscussionTopic( name: child.name, - action: { self.router.showThreads(courseID: self.courseID, - topics: topics, - title: child.name, - type: .courseTopics(topicID: child.id))}, + action: { + self.router.showThreads( + courseID: self.courseID, + topics: topics, + title: child.name, + type: .courseTopics(topicID: child.id) + ) + }, style: .subTopic) ) } @@ -113,17 +121,17 @@ public class DiscussionTopicsViewModel: ObservableObject { public func getTopics(courseID: String, withProgress: Bool = true) async { self.courseID = courseID isShowProgress = withProgress - do { - topics = try await interactor.getTopics(courseID: courseID) - discussionTopics = generateTopics(topics: topics) - isShowProgress = false - } catch let error { - isShowProgress = false - if error.isInternetError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError - } + do { + topics = try await interactor.getTopics(courseID: courseID) + discussionTopics = generateTopics(topics: topics) + isShowProgress = false + } catch let error { + isShowProgress = false + if error.isInternetError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError } + } } } diff --git a/Discussion/Discussion/Presentation/Posts/PostsView.swift b/Discussion/Discussion/Presentation/Posts/PostsView.swift index 1ee9a943b..dcfe705bb 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsView.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsView.swift @@ -104,67 +104,96 @@ public struct PostsView: View { pageNumber: 1, withProgress: isIOS14) }) { - LazyVStack { - VStack {}.frame(height: 1) - .id(1) - let posts = Array(viewModel.filteredPosts.enumerated()) - HStack { - Text(title) - .font(Theme.Fonts.titleLarge) - .foregroundColor(CoreAssets.textPrimary.swiftUIColor) - .padding(.horizontal, 24) - .padding(.top, 12) - Spacer() - } - ForEach(posts, id: \.offset) { index, post in - PostCell(post: post).padding(24) - .onAppear { - Task { - await viewModel.getPostsPagination(courseID: self.courseID, index: index) + let posts = Array(viewModel.filteredPosts.enumerated()) + if posts.count >= 1 { + LazyVStack { + VStack {}.frame(height: 1) + .id(1) + HStack(alignment: .center) { + Text(title) + .font(Theme.Fonts.titleLarge) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) + Spacer() + Button(action: { + router.createNewThread(courseID: courseID, + selectedTopic: currentBlockID, + onPostCreated: { + reloadPage(onSuccess: { + withAnimation { + scroll.scrollTo(1) + } + }) + }) + }, label: { + VStack { + CoreAssets.addComment.swiftUIImage + .font(Theme.Fonts.labelLarge) + .padding(6) } + .foregroundColor(.white) + .background( + Circle() + .foregroundColor(CoreAssets.accentColor.swiftUIColor) + ) + }) + } + .padding(.horizontal, 24) + + ForEach(posts, id: \.offset) { index, post in + PostCell(post: post).padding(24) + .id(UUID()) + .onAppear { + Task { + await viewModel.getPostsPagination( + courseID: self.courseID, + index: index + ) + } + } + if posts.last?.element != post { + Divider().padding(.horizontal, 24) } - if posts.last?.element != post { - Divider().padding(.horizontal, 24) } + Spacer(minLength: 84) + } + } else { + if !viewModel.isShowProgress { + VStack(spacing: 0) { + CoreAssets.discussionIcon.swiftUIImage + .renderingMode(.template) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) + Text(DiscussionLocalization.Posts.NoDiscussion.title) + .font(Theme.Fonts.titleLarge) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.top, 40) + Text(DiscussionLocalization.Posts.NoDiscussion.description) + .font(Theme.Fonts.bodyLarge) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.top, 12) + StyledButton(DiscussionLocalization.Posts.NoDiscussion.createbutton, + action: { + router.createNewThread(courseID: courseID, + selectedTopic: currentBlockID, + onPostCreated: { + reloadPage(onSuccess: { + withAnimation { + scroll.scrollTo(1) + } + }) + }) + }).frame(width: 215).padding(.top, 40) + }.padding(24) + .padding(.top, 100) } - Spacer(minLength: 84) - }.id(UUID()) + } } }.frameLimit() .animation(listAnimation) .onRightSwipeGesture { router.back() } - - VStack { - Spacer() - Button(action: { - router.createNewThread(courseID: courseID, - selectedTopic: currentBlockID, - onPostCreated: { - reloadPage(onSuccess: { - withAnimation { - scroll.scrollTo(1) - } - }) - }) - }, label: { - VStack { - HStack(alignment: .center) { - CoreAssets.addComment.swiftUIImage - .font(Theme.Fonts.labelLarge) - Text(DiscussionLocalization.Posts.createNewPost) - }.frame(maxHeight: 42) - .padding(.horizontal, 20) - } - .foregroundColor(.white) - .background( - Theme.Shapes.buttonShape - .foregroundColor(CoreAssets.accentColor.swiftUIColor) - ) - .padding(.bottom, 30) - }) - } } }.frame(maxWidth: .infinity) } diff --git a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift index 3287b0ee1..7d93b50cd 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift @@ -41,7 +41,7 @@ public class PostsViewModel: ObservableObject { @Published var filterTitle: ThreadsFilter = .allThreads { willSet { if let courseID { - resetPosts() + resetPosts() Task { _ = await getPosts(courseID: courseID, pageNumber: 1) } @@ -49,15 +49,15 @@ public class PostsViewModel: ObservableObject { } } @Published var sortTitle: SortType = .recentActivity { - willSet { - if let courseID { - resetPosts() - Task { - _ = await getPosts(courseID: courseID, pageNumber: 1) - } - } - } - } + willSet { + if let courseID { + resetPosts() + Task { + _ = await getPosts(courseID: courseID, pageNumber: 1) + } + } + } + } @Published var filterButtons: [ActionSheet.Button] = [] public var courseID: String? @@ -80,9 +80,11 @@ public class PostsViewModel: ObservableObject { internal let postStateSubject = CurrentValueSubject(nil) private var cancellable: AnyCancellable? - public init(interactor: DiscussionInteractorProtocol, - router: DiscussionRouter, - config: Config) { + public init( + interactor: DiscussionInteractorProtocol, + router: DiscussionRouter, + config: Config + ) { self.interactor = interactor self.router = router self.config = config @@ -171,7 +173,7 @@ public class PostsViewModel: ObservableObject { func getPostsPagination(courseID: String, index: Int, withProgress: Bool = true) async { if !fetchInProgress { if totalPages > 1 { - if index == threads.threads.count - 3 { + if index == filteredPosts.count - 3 { if totalPages != 1 { if nextPage <= totalPages { _ = await getPosts(courseID: courseID, @@ -186,6 +188,7 @@ public class PostsViewModel: ObservableObject { @MainActor public func getPosts(courseID: String, pageNumber: Int, withProgress: Bool = true) async -> Bool { + fetchInProgress = true isShowProgress = withProgress do { switch type { @@ -199,6 +202,7 @@ public class PostsViewModel: ObservableObject { if threads.threads.indices.contains(0) { self.totalPages = threads.threads[0].numPages self.nextPage += 1 + fetchInProgress = false } case .followingPosts: threads.threads += try await interactor @@ -210,6 +214,7 @@ public class PostsViewModel: ObservableObject { if threads.threads.indices.contains(0) { self.totalPages = threads.threads[0].numPages self.nextPage += 1 + fetchInProgress = false } case .nonCourseTopics: threads.threads += try await interactor @@ -221,6 +226,7 @@ public class PostsViewModel: ObservableObject { if threads.threads.indices.contains(0) { self.totalPages = threads.threads[0].numPages self.nextPage += 1 + fetchInProgress = false } case .courseTopics(topicID: let topicID): threads.threads += try await interactor @@ -232,6 +238,7 @@ public class PostsViewModel: ObservableObject { if threads.threads.indices.contains(0) { self.totalPages = threads.threads[0].numPages self.nextPage += 1 + fetchInProgress = false } case .none: isShowProgress = false diff --git a/Discussion/Discussion/SwiftGen/Strings.swift b/Discussion/Discussion/SwiftGen/Strings.swift index d23922bea..61b06aecd 100644 --- a/Discussion/Discussion/SwiftGen/Strings.swift +++ b/Discussion/Discussion/SwiftGen/Strings.swift @@ -86,6 +86,14 @@ public enum DiscussionLocalization { /// Unread public static let unread = DiscussionLocalization.tr("Localizable", "POSTS.FILTER.UNREAD", fallback: "Unread") } + public enum NoDiscussion { + /// Create discussion + public static let createbutton = DiscussionLocalization.tr("Localizable", "POSTS.NO_DISCUSSION.CREATEBUTTON", fallback: "Create discussion") + /// Click the button below to create your first discussion. + public static let description = DiscussionLocalization.tr("Localizable", "POSTS.NO_DISCUSSION.DESCRIPTION", fallback: "Click the button below to create your first discussion.") + /// No discussions yet + public static let title = DiscussionLocalization.tr("Localizable", "POSTS.NO_DISCUSSION.TITLE", fallback: "No discussions yet") + } public enum Sort { /// Most Activity public static let mostActivity = DiscussionLocalization.tr("Localizable", "POSTS.SORT.MOST_ACTIVITY", fallback: "Most Activity") diff --git a/Discussion/Discussion/en.lproj/Localizable.strings b/Discussion/Discussion/en.lproj/Localizable.strings index 1e9b4413a..110b0cae5 100644 --- a/Discussion/Discussion/en.lproj/Localizable.strings +++ b/Discussion/Discussion/en.lproj/Localizable.strings @@ -16,6 +16,9 @@ "POSTS.SORT.RECENT_ACTIVITY" = "Recent Activity"; "POSTS.SORT.MOST_ACTIVITY" = "Most Activity"; "POSTS.SORT.MOST_VOTES" = "Most Votes"; +"POSTS.NO_DISCUSSION.TITLE" = "No discussions yet"; +"POSTS.NO_DISCUSSION.DESCRIPTION" = "Click the button below to create your first discussion."; +"POSTS.NO_DISCUSSION.CREATEBUTTON" = "Create discussion"; "POSTS.FILTER.ALL_POSTS" = "All Posts"; "POSTS.FILTER.UNREAD" = "Unread"; diff --git a/Discussion/Discussion/uk.lproj/Localizable.strings b/Discussion/Discussion/uk.lproj/Localizable.strings index fbb1300dd..f54f4796f 100644 --- a/Discussion/Discussion/uk.lproj/Localizable.strings +++ b/Discussion/Discussion/uk.lproj/Localizable.strings @@ -20,6 +20,9 @@ "POSTS.FILTER.ALL_POSTS" = "Всі пости"; "POSTS.FILTER.UNREAD" = "Непрочитаних"; "POSTS.FILTER.UNANSWERED" = "Без відповіді"; +"POSTS.NO_DISCUSSION.TITLE" = "Ще немає дискусій"; +"POSTS.NO_DISCUSSION.DESCRIPTION" = "Натисніть кнопку нижче, щоб створити свою першу дискусію."; +"POSTS.NO_DISCUSSION.CREATEBUTTON" = "Створити дискусію"; "POSTS.CREATE_NEW_POST" = "Створити новий пост"; "POSTS.ALERT.MAKE_SELECTION" = "Оберіть"; diff --git a/Discussion/DiscussionTests/DiscussionMock.generated.swift b/Discussion/DiscussionTests/DiscussionMock.generated.swift index f08d3bc5a..868fa3963 100644 --- a/Discussion/DiscussionTests/DiscussionMock.generated.swift +++ b/Discussion/DiscussionTests/DiscussionMock.generated.swift @@ -514,10 +514,10 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`) } - open func presentAlert(alertTitle: String, alertMessage: String, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void) { - addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) - let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) as? (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void - perform?(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`) + open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void + perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`) } open func presentView(transitionStyle: UIModalTransitionStyle, view: any View) { @@ -544,7 +544,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showRegisterScreen case m_showForgotPasswordScreen case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) - case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) case m_presentView__transitionStyle_transitionStylecontent_content(Parameter, Parameter<() -> any View>) @@ -590,14 +590,16 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) return Matcher.ComparisonResult(results) - case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped)): + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsNextsectionname, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped, let lhsNextsectiontapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsNextsectionname, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped, let rhsNextsectiontapped)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectionname, rhs: rhsNextsectionname, with: matcher), lhsNextsectionname, rhsNextsectionname, "nextSectionName")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsImage, rhs: rhsImage, with: matcher), lhsImage, rhsImage, "image")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectiontapped, rhs: rhsNextsectiontapped, with: matcher), lhsNextsectiontapped, rhsNextsectiontapped, "nextSectionTapped")) return Matcher.ComparisonResult(results) case (.m_presentView__transitionStyle_transitionStyleview_view(let lhsTransitionstyle, let lhsView), .m_presentView__transitionStyle_transitionStyleview_view(let rhsTransitionstyle, let rhsView)): @@ -627,7 +629,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return 0 case .m_showForgotPasswordScreen: return 0 case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue - case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue case let .m_presentView__transitionStyle_transitionStylecontent_content(p0, p1): return p0.intValue + p1.intValue } @@ -644,7 +646,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return ".showRegisterScreen()" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" - case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped: return ".presentAlert(alertTitle:alertMessage:action:image:onCloseTapped:okTapped:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" case .m_presentView__transitionStyle_transitionStylecontent_content: return ".presentView(transitionStyle:content:)" } @@ -675,7 +677,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} public static func presentView(transitionStyle: Parameter, content: Parameter<() -> any View>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStylecontent_content(`transitionStyle`, `content`))} } @@ -714,8 +716,8 @@ open class BaseRouterMock: BaseRouter, Mock { public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, perform: @escaping (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { - return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`), performs: perform) + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>, perform: @escaping (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`), performs: perform) } public static func presentView(transitionStyle: Parameter, view: Parameter, perform: @escaping (UIModalTransitionStyle, any View) -> Void) -> Perform { return Perform(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`), performs: perform) @@ -1038,16 +1040,16 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock - open func getThreadsList(courseID: String, type: ThreadType, filter: ThreadsFilter, page: Int) throws -> ThreadLists { - addInvocation(.m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(Parameter.value(`courseID`), Parameter.value(`type`), Parameter.value(`filter`), Parameter.value(`page`))) - let perform = methodPerformValue(.m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(Parameter.value(`courseID`), Parameter.value(`type`), Parameter.value(`filter`), Parameter.value(`page`))) as? (String, ThreadType, ThreadsFilter, Int) -> Void - perform?(`courseID`, `type`, `filter`, `page`) + open func getThreadsList(courseID: String, type: ThreadType, sort: SortType, filter: ThreadsFilter, page: Int) throws -> ThreadLists { + addInvocation(.m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(Parameter.value(`courseID`), Parameter.value(`type`), Parameter.value(`sort`), Parameter.value(`filter`), Parameter.value(`page`))) + let perform = methodPerformValue(.m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(Parameter.value(`courseID`), Parameter.value(`type`), Parameter.value(`sort`), Parameter.value(`filter`), Parameter.value(`page`))) as? (String, ThreadType, SortType, ThreadsFilter, Int) -> Void + perform?(`courseID`, `type`, `sort`, `filter`, `page`) var __value: ThreadLists do { - __value = try methodReturnValue(.m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(Parameter.value(`courseID`), Parameter.value(`type`), Parameter.value(`filter`), Parameter.value(`page`))).casted() + __value = try methodReturnValue(.m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(Parameter.value(`courseID`), Parameter.value(`type`), Parameter.value(`sort`), Parameter.value(`filter`), Parameter.value(`page`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getThreadsList(courseID: String, type: ThreadType, filter: ThreadsFilter, page: Int). Use given") - Failure("Stub return value not specified for getThreadsList(courseID: String, type: ThreadType, filter: ThreadsFilter, page: Int). Use given") + onFatalFailure("Stub return value not specified for getThreadsList(courseID: String, type: ThreadType, sort: SortType, filter: ThreadsFilter, page: Int). Use given") + Failure("Stub return value not specified for getThreadsList(courseID: String, type: ThreadType, sort: SortType, filter: ThreadsFilter, page: Int). Use given") } catch { throw error } @@ -1086,11 +1088,11 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock return __value } - open func getDiscussionComments(threadID: String, page: Int) throws -> ([UserComment], Int) { + open func getDiscussionComments(threadID: String, page: Int) throws -> ([UserComment], Pagination) { addInvocation(.m_getDiscussionComments__threadID_threadIDpage_page(Parameter.value(`threadID`), Parameter.value(`page`))) let perform = methodPerformValue(.m_getDiscussionComments__threadID_threadIDpage_page(Parameter.value(`threadID`), Parameter.value(`page`))) as? (String, Int) -> Void perform?(`threadID`, `page`) - var __value: ([UserComment], Int) + var __value: ([UserComment], Pagination) do { __value = try methodReturnValue(.m_getDiscussionComments__threadID_threadIDpage_page(Parameter.value(`threadID`), Parameter.value(`page`))).casted() } catch MockError.notStubed { @@ -1102,11 +1104,11 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock return __value } - open func getQuestionComments(threadID: String, page: Int) throws -> ([UserComment], Int) { + open func getQuestionComments(threadID: String, page: Int) throws -> ([UserComment], Pagination) { addInvocation(.m_getQuestionComments__threadID_threadIDpage_page(Parameter.value(`threadID`), Parameter.value(`page`))) let perform = methodPerformValue(.m_getQuestionComments__threadID_threadIDpage_page(Parameter.value(`threadID`), Parameter.value(`page`))) as? (String, Int) -> Void perform?(`threadID`, `page`) - var __value: ([UserComment], Int) + var __value: ([UserComment], Pagination) do { __value = try methodReturnValue(.m_getQuestionComments__threadID_threadIDpage_page(Parameter.value(`threadID`), Parameter.value(`page`))).casted() } catch MockError.notStubed { @@ -1118,11 +1120,11 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock return __value } - open func getCommentResponses(commentID: String, page: Int) throws -> ([UserComment], Int) { + open func getCommentResponses(commentID: String, page: Int) throws -> ([UserComment], Pagination) { addInvocation(.m_getCommentResponses__commentID_commentIDpage_page(Parameter.value(`commentID`), Parameter.value(`page`))) let perform = methodPerformValue(.m_getCommentResponses__commentID_commentIDpage_page(Parameter.value(`commentID`), Parameter.value(`page`))) as? (String, Int) -> Void perform?(`commentID`, `page`) - var __value: ([UserComment], Int) + var __value: ([UserComment], Pagination) do { __value = try methodReturnValue(.m_getCommentResponses__commentID_commentIDpage_page(Parameter.value(`commentID`), Parameter.value(`page`))).casted() } catch MockError.notStubed { @@ -1243,7 +1245,7 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock fileprivate enum MethodType { - case m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(Parameter, Parameter, Parameter, Parameter) + case m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(Parameter, Parameter, Parameter, Parameter, Parameter) case m_getTopics__courseID_courseID(Parameter) case m_searchThreads__courseID_courseIDsearchText_searchTextpageNumber_pageNumber(Parameter, Parameter, Parameter) case m_getDiscussionComments__threadID_threadIDpage_page(Parameter, Parameter) @@ -1260,10 +1262,11 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { - case (.m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(let lhsCourseid, let lhsType, let lhsFilter, let lhsPage), .m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(let rhsCourseid, let rhsType, let rhsFilter, let rhsPage)): + case (.m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(let lhsCourseid, let lhsType, let lhsSort, let lhsFilter, let lhsPage), .m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(let rhsCourseid, let rhsType, let rhsSort, let rhsFilter, let rhsPage)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSort, rhs: rhsSort, with: matcher), lhsSort, rhsSort, "sort")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFilter, rhs: rhsFilter, with: matcher), lhsFilter, rhsFilter, "filter")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPage, rhs: rhsPage, with: matcher), lhsPage, rhsPage, "page")) return Matcher.ComparisonResult(results) @@ -1350,7 +1353,7 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock func intValue() -> Int { switch self { - case let .m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(p0, p1, p2, p3, p4): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue case let .m_getTopics__courseID_courseID(p0): return p0.intValue case let .m_searchThreads__courseID_courseIDsearchText_searchTextpageNumber_pageNumber(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_getDiscussionComments__threadID_threadIDpage_page(p0, p1): return p0.intValue + p1.intValue @@ -1368,7 +1371,7 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock } func assertionName() -> String { switch self { - case .m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page: return ".getThreadsList(courseID:type:filter:page:)" + case .m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page: return ".getThreadsList(courseID:type:sort:filter:page:)" case .m_getTopics__courseID_courseID: return ".getTopics(courseID:)" case .m_searchThreads__courseID_courseIDsearchText_searchTextpageNumber_pageNumber: return ".searchThreads(courseID:searchText:pageNumber:)" case .m_getDiscussionComments__threadID_threadIDpage_page: return ".getDiscussionComments(threadID:page:)" @@ -1395,8 +1398,8 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock } - public static func getThreadsList(courseID: Parameter, type: Parameter, filter: Parameter, page: Parameter, willReturn: ThreadLists...) -> MethodStub { - return Given(method: .m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(`courseID`, `type`, `filter`, `page`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func getThreadsList(courseID: Parameter, type: Parameter, sort: Parameter, filter: Parameter, page: Parameter, willReturn: ThreadLists...) -> MethodStub { + return Given(method: .m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(`courseID`, `type`, `sort`, `filter`, `page`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getTopics(courseID: Parameter, willReturn: Topics...) -> MethodStub { return Given(method: .m_getTopics__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -1404,24 +1407,24 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock public static func searchThreads(courseID: Parameter, searchText: Parameter, pageNumber: Parameter, willReturn: ThreadLists...) -> MethodStub { return Given(method: .m_searchThreads__courseID_courseIDsearchText_searchTextpageNumber_pageNumber(`courseID`, `searchText`, `pageNumber`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getDiscussionComments(threadID: Parameter, page: Parameter, willReturn: ([UserComment], Int)...) -> MethodStub { + public static func getDiscussionComments(threadID: Parameter, page: Parameter, willReturn: ([UserComment], Pagination)...) -> MethodStub { return Given(method: .m_getDiscussionComments__threadID_threadIDpage_page(`threadID`, `page`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getQuestionComments(threadID: Parameter, page: Parameter, willReturn: ([UserComment], Int)...) -> MethodStub { + public static func getQuestionComments(threadID: Parameter, page: Parameter, willReturn: ([UserComment], Pagination)...) -> MethodStub { return Given(method: .m_getQuestionComments__threadID_threadIDpage_page(`threadID`, `page`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getCommentResponses(commentID: Parameter, page: Parameter, willReturn: ([UserComment], Int)...) -> MethodStub { + public static func getCommentResponses(commentID: Parameter, page: Parameter, willReturn: ([UserComment], Pagination)...) -> MethodStub { return Given(method: .m_getCommentResponses__commentID_commentIDpage_page(`commentID`, `page`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func addCommentTo(threadID: Parameter, rawBody: Parameter, parentID: Parameter, willReturn: Post...) -> MethodStub { return Given(method: .m_addCommentTo__threadID_threadIDrawBody_rawBodyparentID_parentID(`threadID`, `rawBody`, `parentID`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getThreadsList(courseID: Parameter, type: Parameter, filter: Parameter, page: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(`courseID`, `type`, `filter`, `page`), products: willThrow.map({ StubProduct.throw($0) })) + public static func getThreadsList(courseID: Parameter, type: Parameter, sort: Parameter, filter: Parameter, page: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(`courseID`, `type`, `sort`, `filter`, `page`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getThreadsList(courseID: Parameter, type: Parameter, filter: Parameter, page: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func getThreadsList(courseID: Parameter, type: Parameter, sort: Parameter, filter: Parameter, page: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(`courseID`, `type`, `filter`, `page`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(`courseID`, `type`, `sort`, `filter`, `page`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (ThreadLists).self) willProduce(stubber) return given @@ -1449,30 +1452,30 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock public static func getDiscussionComments(threadID: Parameter, page: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_getDiscussionComments__threadID_threadIDpage_page(`threadID`, `page`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getDiscussionComments(threadID: Parameter, page: Parameter, willProduce: (StubberThrows<([UserComment], Int)>) -> Void) -> MethodStub { + public static func getDiscussionComments(threadID: Parameter, page: Parameter, willProduce: (StubberThrows<([UserComment], Pagination)>) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_getDiscussionComments__threadID_threadIDpage_page(`threadID`, `page`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (([UserComment], Int)).self) + let stubber = given.stubThrows(for: (([UserComment], Pagination)).self) willProduce(stubber) return given } public static func getQuestionComments(threadID: Parameter, page: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_getQuestionComments__threadID_threadIDpage_page(`threadID`, `page`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getQuestionComments(threadID: Parameter, page: Parameter, willProduce: (StubberThrows<([UserComment], Int)>) -> Void) -> MethodStub { + public static func getQuestionComments(threadID: Parameter, page: Parameter, willProduce: (StubberThrows<([UserComment], Pagination)>) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_getQuestionComments__threadID_threadIDpage_page(`threadID`, `page`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (([UserComment], Int)).self) + let stubber = given.stubThrows(for: (([UserComment], Pagination)).self) willProduce(stubber) return given } public static func getCommentResponses(commentID: Parameter, page: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_getCommentResponses__commentID_commentIDpage_page(`commentID`, `page`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getCommentResponses(commentID: Parameter, page: Parameter, willProduce: (StubberThrows<([UserComment], Int)>) -> Void) -> MethodStub { + public static func getCommentResponses(commentID: Parameter, page: Parameter, willProduce: (StubberThrows<([UserComment], Pagination)>) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_getCommentResponses__commentID_commentIDpage_page(`commentID`, `page`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (([UserComment], Int)).self) + let stubber = given.stubThrows(for: (([UserComment], Pagination)).self) willProduce(stubber) return given } @@ -1561,7 +1564,7 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock public struct Verify { fileprivate var method: MethodType - public static func getThreadsList(courseID: Parameter, type: Parameter, filter: Parameter, page: Parameter) -> Verify { return Verify(method: .m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(`courseID`, `type`, `filter`, `page`))} + public static func getThreadsList(courseID: Parameter, type: Parameter, sort: Parameter, filter: Parameter, page: Parameter) -> Verify { return Verify(method: .m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(`courseID`, `type`, `sort`, `filter`, `page`))} public static func getTopics(courseID: Parameter) -> Verify { return Verify(method: .m_getTopics__courseID_courseID(`courseID`))} public static func searchThreads(courseID: Parameter, searchText: Parameter, pageNumber: Parameter) -> Verify { return Verify(method: .m_searchThreads__courseID_courseIDsearchText_searchTextpageNumber_pageNumber(`courseID`, `searchText`, `pageNumber`))} public static func getDiscussionComments(threadID: Parameter, page: Parameter) -> Verify { return Verify(method: .m_getDiscussionComments__threadID_threadIDpage_page(`threadID`, `page`))} @@ -1581,8 +1584,8 @@ open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock fileprivate var method: MethodType var performs: Any - public static func getThreadsList(courseID: Parameter, type: Parameter, filter: Parameter, page: Parameter, perform: @escaping (String, ThreadType, ThreadsFilter, Int) -> Void) -> Perform { - return Perform(method: .m_getThreadsList__courseID_courseIDtype_typefilter_filterpage_page(`courseID`, `type`, `filter`, `page`), performs: perform) + public static func getThreadsList(courseID: Parameter, type: Parameter, sort: Parameter, filter: Parameter, page: Parameter, perform: @escaping (String, ThreadType, SortType, ThreadsFilter, Int) -> Void) -> Perform { + return Perform(method: .m_getThreadsList__courseID_courseIDtype_typesort_sortfilter_filterpage_page(`courseID`, `type`, `sort`, `filter`, `page`), performs: perform) } public static func getTopics(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_getTopics__courseID_courseID(`courseID`), performs: perform) @@ -1832,10 +1835,10 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { perform?(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`) } - open func presentAlert(alertTitle: String, alertMessage: String, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void) { - addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) - let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) as? (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void - perform?(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`) + open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void + perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`) } open func presentView(transitionStyle: UIModalTransitionStyle, view: any View) { @@ -1867,7 +1870,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case m_showRegisterScreen case m_showForgotPasswordScreen case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) - case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) case m_presentView__transitionStyle_transitionStylecontent_content(Parameter, Parameter<() -> any View>) @@ -1946,14 +1949,16 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) return Matcher.ComparisonResult(results) - case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped)): + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsNextsectionname, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped, let lhsNextsectiontapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsNextsectionname, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped, let rhsNextsectiontapped)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectionname, rhs: rhsNextsectionname, with: matcher), lhsNextsectionname, rhsNextsectionname, "nextSectionName")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsImage, rhs: rhsImage, with: matcher), lhsImage, rhsImage, "image")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectiontapped, rhs: rhsNextsectiontapped, with: matcher), lhsNextsectiontapped, rhsNextsectiontapped, "nextSectionTapped")) return Matcher.ComparisonResult(results) case (.m_presentView__transitionStyle_transitionStyleview_view(let lhsTransitionstyle, let lhsView), .m_presentView__transitionStyle_transitionStyleview_view(let rhsTransitionstyle, let rhsView)): @@ -1988,7 +1993,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case .m_showRegisterScreen: return 0 case .m_showForgotPasswordScreen: return 0 case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue - case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue case let .m_presentView__transitionStyle_transitionStylecontent_content(p0, p1): return p0.intValue + p1.intValue } @@ -2010,7 +2015,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case .m_showRegisterScreen: return ".showRegisterScreen()" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" - case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped: return ".presentAlert(alertTitle:alertMessage:action:image:onCloseTapped:okTapped:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" case .m_presentView__transitionStyle_transitionStylecontent_content: return ".presentView(transitionStyle:content:)" } @@ -2046,7 +2051,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} public static func presentView(transitionStyle: Parameter, content: Parameter<() -> any View>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStylecontent_content(`transitionStyle`, `content`))} } @@ -2100,8 +2105,8 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, perform: @escaping (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { - return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`), performs: perform) + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>, perform: @escaping (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`), performs: perform) } public static func presentView(transitionStyle: Parameter, view: Parameter, perform: @escaping (UIModalTransitionStyle, any View) -> Void) -> Perform { return Perform(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`), performs: perform) diff --git a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift index fe84c0c57..ee4da8ee4 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift @@ -41,12 +41,14 @@ final class BaseResponsesViewModelTests: XCTestCase { threadID: "1", commentID: "1", parentID: nil, - abuseFlagged: false) + abuseFlagged: false, + closed: false) ], threadID: "1", commentID: "1", parentID: nil, - abuseFlagged: false) + abuseFlagged: false, + closed: false) func testVoteThreadSuccess() async throws { let interactor = DiscussionInteractorProtocolMock() @@ -348,7 +350,8 @@ final class BaseResponsesViewModelTests: XCTestCase { threadID: "3", commentID: "3", parentID: nil, - abuseFlagged: false) + abuseFlagged: false, + closed: false) viewModel.addNewPost(newPost) diff --git a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift index 774039847..d0909522b 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift @@ -76,6 +76,7 @@ final class ThreadViewModelTests: XCTestCase { type: .question, title: "1", pinned: false, + closed: false, following: false, commentCount: 1, avatar: "1", @@ -96,6 +97,7 @@ final class ThreadViewModelTests: XCTestCase { type: .discussion, title: "2", pinned: false, + closed: false, following: false, commentCount: 2, avatar: "2", @@ -116,6 +118,7 @@ final class ThreadViewModelTests: XCTestCase { type: .discussion, title: "3", pinned: false, + closed: false, following: false, commentCount: 3, avatar: "3", @@ -136,6 +139,7 @@ final class ThreadViewModelTests: XCTestCase { type: .question, title: "4", pinned: false, + closed: false, following: false, commentCount: 1, avatar: "4", @@ -172,7 +176,8 @@ final class ThreadViewModelTests: XCTestCase { threadID: "2", commentID: "2", parentID: nil, - abuseFlagged: false), + abuseFlagged: false, + closed: false), Post(authorName: "2", authorAvatar: "2", postDate: Date(), @@ -188,12 +193,14 @@ final class ThreadViewModelTests: XCTestCase { threadID: "2", commentID: "2", parentID: nil, - abuseFlagged: false) + abuseFlagged: false, + closed: false) ], threadID: "1", commentID: "1", parentID: nil, - abuseFlagged: false + abuseFlagged: false, + closed: false ) func testGetQuestionPostsSuccess() async { @@ -209,8 +216,12 @@ final class ThreadViewModelTests: XCTestCase { postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willProduce: {_ in})) - Given(interactor, .getQuestionComments(threadID: .any, page: .any, willReturn: (userComments, 1))) - + Given(interactor, .getQuestionComments(threadID: .any, page: .any, + willReturn: (userComments, Pagination(next: "", + previous: "", + count: 1, + numPages: 1)))) + result = await viewModel.getPosts(thread: threads.threads[0], page: 1) Verify(interactor, .readBody(threadID: .value(threads.threads[0].id))) @@ -235,7 +246,11 @@ final class ThreadViewModelTests: XCTestCase { postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willProduce: {_ in})) - Given(interactor, .getDiscussionComments(threadID: .any, page: .any, willReturn: (userComments, 1))) + Given(interactor, .getDiscussionComments(threadID: .any, page: .any, + willReturn: (userComments, Pagination(next: "", + previous: "", + count: 1, + numPages: 1)))) result = await viewModel.getPosts(thread: threads.threads[1], page: 1) @@ -335,7 +350,8 @@ final class ThreadViewModelTests: XCTestCase { threadID: "", commentID: "", parentID: nil, - abuseFlagged: true) + abuseFlagged: true, + closed: false) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willReturn: post) ) @@ -412,7 +428,11 @@ final class ThreadViewModelTests: XCTestCase { viewModel.totalPages = 2 viewModel.comments = userComments + userComments - Given(interactor, .getQuestionComments(threadID: .any, page: .any, willReturn: (userComments, 1))) + Given(interactor, .getQuestionComments(threadID: .any, page: .any, + willReturn: (userComments, Pagination(next: "", + previous: "", + count: 1, + numPages: 1)))) result = await viewModel.fetchMorePosts(thread: threads.threads[0], index: 3) diff --git a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift index 4297f2e52..f94484fc2 100644 --- a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift @@ -37,6 +37,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { type: .discussion, title: "1", pinned: false, + closed: false, following: true, commentCount: 1, avatar: "avatar", @@ -74,31 +75,6 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { debounce: .test) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) - - let items = ThreadLists( - threads: [ - UserThread(id: "1", - author: "1", - authorLabel: "1", - createdAt: Date(), - updatedAt: Date(), - rawBody: "1", - renderedBody: "1", - voted: false, - voteCount: 1, - courseID: "1", - type: .discussion, - title: "1", - pinned: false, - following: true, - commentCount: 1, - avatar: "avatar", - unreadCommentCount: 1, - abuseFlagged: false, - hasEndorsed: true, - numPages: 1) - ] - ) Given(interactor, .searchThreads(courseID: .any, searchText: .any, pageNumber: .any, willThrow: noInternetError)) @@ -126,31 +102,6 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { interactor: interactor, router: router, debounce: .test) - - let items = ThreadLists( - threads: [ - UserThread(id: "1", - author: "1", - authorLabel: "1", - createdAt: Date(), - updatedAt: Date(), - rawBody: "1", - renderedBody: "1", - voted: false, - voteCount: 1, - courseID: "1", - type: .discussion, - title: "1", - pinned: false, - following: true, - commentCount: 1, - avatar: "avatar", - unreadCommentCount: 1, - abuseFlagged: false, - hasEndorsed: true, - numPages: 1) - ] - ) Given(interactor, .searchThreads(courseID: .any, searchText: .any, pageNumber: .any, willThrow: NSError())) diff --git a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift index 3250b8d9d..cd4dbab3c 100644 --- a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift @@ -28,6 +28,7 @@ final class PostViewModelTests: XCTestCase { type: .question, title: "1", pinned: false, + closed: false, following: false, commentCount: 1, avatar: "1", @@ -48,6 +49,7 @@ final class PostViewModelTests: XCTestCase { type: .discussion, title: "2", pinned: false, + closed: false, following: false, commentCount: 2, avatar: "2", @@ -68,6 +70,7 @@ final class PostViewModelTests: XCTestCase { type: .question, title: "3", pinned: false, + closed: false, following: false, commentCount: 3, avatar: "3", @@ -88,6 +91,7 @@ final class PostViewModelTests: XCTestCase { type: .question, title: "4", pinned: false, + closed: false, following: false, commentCount: 4, avatar: "4", @@ -106,7 +110,7 @@ final class PostViewModelTests: XCTestCase { viewModel.type = .allPosts - Given(interactor, .getThreadsList(courseID: .any, type: .any, filter: .any, page: .any, willReturn: threads)) + Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willReturn: threads)) viewModel.type = .allPosts result = await viewModel.getPosts(courseID: "1", pageNumber: 1) @@ -132,7 +136,7 @@ final class PostViewModelTests: XCTestCase { result = await viewModel.getPosts(courseID: "1", pageNumber: 1) XCTAssertFalse(result) - Verify(interactor, 4, .getThreadsList(courseID: .value("1"), type: .any, filter: .any, page: .value(1))) + Verify(interactor, 4, .getThreadsList(courseID: .value("1"), type: .any, sort: .any, filter: .any, page: .value(1))) XCTAssertFalse(viewModel.isShowProgress) XCTAssertFalse(viewModel.showError) @@ -148,12 +152,12 @@ final class PostViewModelTests: XCTestCase { let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) - Given(interactor, .getThreadsList(courseID: .any, type: .any, filter: .any, page: .any, willThrow: noInternetError)) + Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willThrow: noInternetError)) viewModel.type = .allPosts result = await viewModel.getPosts(courseID: "1", pageNumber: 1) - Verify(interactor, 1, .getThreadsList(courseID: .any, type: .any, filter: .any, page: .any)) + Verify(interactor, 1, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any)) XCTAssertFalse(result) XCTAssertFalse(viewModel.isShowProgress) @@ -168,12 +172,12 @@ final class PostViewModelTests: XCTestCase { var result = false let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) - Given(interactor, .getThreadsList(courseID: .any, type: .any, filter: .any, page: .any, willThrow: NSError())) + Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willThrow: NSError())) viewModel.type = .allPosts result = await viewModel.getPosts(courseID: "1", pageNumber: 1) - Verify(interactor, 1, .getThreadsList(courseID: .any, type: .any, filter: .any, page: .any)) + Verify(interactor, 1, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any)) XCTAssertFalse(result) XCTAssertFalse(viewModel.isShowProgress) @@ -187,26 +191,32 @@ final class PostViewModelTests: XCTestCase { let config = ConfigMock() let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) - Given(interactor, .getThreadsList(courseID: .any, type: .any, filter: .any, page: .any, + Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willReturn: threads)) viewModel.type = .allPosts viewModel.sortTitle = .mostActivity _ = await viewModel.getPosts(courseID: "1", pageNumber: 1) - XCTAssertTrue(viewModel.filteredPosts[0].title == "4") + XCTAssertTrue(viewModel.filteredPosts[0].title == "1") + + Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .value(.recentActivity), filter: .any, page: .any, + willReturn: threads)) viewModel.filterTitle = .unread viewModel.sortTitle = .recentActivity _ = await viewModel.getPosts(courseID: "1", pageNumber: 1) - XCTAssertTrue(viewModel.filteredPosts[0].title == "2") + XCTAssertTrue(viewModel.filteredPosts[0].title == "1") XCTAssertNotNil(viewModel.filteredPosts.first(where: {$0.unreadCommentCount == 4})) + Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .value(.mostVotes), filter: .any, page: .any, + willReturn: threads)) + viewModel.filterTitle = .unanswered viewModel.sortTitle = .mostVotes _ = await viewModel.getPosts(courseID: "1", pageNumber: 1) - XCTAssertTrue(viewModel.filteredPosts[0].title == "3") + XCTAssertTrue(viewModel.filteredPosts[0].title == "1") XCTAssertNotNil(viewModel.filteredPosts.first(where: { $0.hasEndorsed })) - Verify(interactor, .getThreadsList(courseID: .any, type: .any, filter: .any, page: .any)) + Verify(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any)) } } diff --git a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift index 6e9a0c9e1..183174526 100644 --- a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift @@ -89,12 +89,14 @@ final class ResponsesViewModelTests: XCTestCase { threadID: "1", commentID: "1", parentID: nil, - abuseFlagged: false) + abuseFlagged: false, + closed: false) ], threadID: "1", commentID: "1", parentID: nil, - abuseFlagged: false + abuseFlagged: false, + closed: false ) func testGetCommentsSuccess() async throws { @@ -109,7 +111,11 @@ final class ResponsesViewModelTests: XCTestCase { storage: .mock, threadStateSubject: .init(.postAdded(id: "1"))) - Given(interactor, .getCommentResponses(commentID: .any, page: .any, willReturn: (userComments, 1))) + Given(interactor, .getCommentResponses(commentID: .any, page: .any, + willReturn: (userComments, Pagination(next: "", + previous: "", + count: 1, + numPages: 1)))) result = await viewModel.getComments(commentID: "1", parentComment: post, page: 1) @@ -255,7 +261,11 @@ final class ResponsesViewModelTests: XCTestCase { viewModel.totalPages = 2 viewModel.comments = userComments - Given(interactor, .getCommentResponses(commentID: .any, page: .any, willReturn: (userComments, 0))) + Given(interactor, .getCommentResponses(commentID: .any, page: .any, + willReturn: (userComments, Pagination(next: "", + previous: "", + count: 1, + numPages: 1)))) await viewModel.fetchMorePosts(commentID: "1", parentComment: post, index: 0) diff --git a/NewEdX/AppDelegate.swift b/NewEdX/AppDelegate.swift index 25fa5976b..a52d40c6e 100644 --- a/NewEdX/AppDelegate.swift +++ b/NewEdX/AppDelegate.swift @@ -18,6 +18,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? + private var orientationLock: UIInterfaceOrientationMask = .portrait + private var assembler: Assembler? private var lastForceLogoutTime: TimeInterval = 0 @@ -44,6 +46,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + func application( + _ application: UIApplication, + supportedInterfaceOrientationsFor window: UIWindow? + ) -> UIInterfaceOrientationMask { + //Allows external windows, such as WebView Player, to work in any orientation + if window == self.window { + return UIDevice.current.userInterfaceIdiom == .phone ? orientationLock : .all + } else { + return UIDevice.current.userInterfaceIdiom == .phone ? .allButUpsideDown : .all + } + } + private func initDI() { let navigation = UINavigationController() navigation.modalPresentationStyle = .fullScreen diff --git a/NewEdX/CoreDataHandler.swift b/NewEdX/CoreDataHandler.swift index dd9480756..da1a19ac7 100644 --- a/NewEdX/CoreDataHandler.swift +++ b/NewEdX/CoreDataHandler.swift @@ -17,9 +17,11 @@ class CoreDataHandler: CoreDataHandlerProtocol { private let discoveryPersistence: DiscoveryPersistenceProtocol private let coursePersistence: CoursePersistenceProtocol - init(dashboardPersistence: DashboardPersistenceProtocol, - discoveryPersistence: DiscoveryPersistenceProtocol, - coursePersistence: CoursePersistenceProtocol) { + init( + dashboardPersistence: DashboardPersistenceProtocol, + discoveryPersistence: DiscoveryPersistenceProtocol, + coursePersistence: CoursePersistenceProtocol + ) { self.dashboardPersistence = dashboardPersistence self.discoveryPersistence = discoveryPersistence self.coursePersistence = coursePersistence diff --git a/NewEdX/DI/AppAssembly.swift b/NewEdX/DI/AppAssembly.swift index 255da5901..da342ccf2 100644 --- a/NewEdX/DI/AppAssembly.swift +++ b/NewEdX/DI/AppAssembly.swift @@ -16,6 +16,7 @@ import Discussion import Authorization import Profile +// swiftlint:disable function_body_length class AppAssembly: Assembly { private let navigation: UINavigationController @@ -90,7 +91,8 @@ class AppAssembly: Assembly { container.register(AppStorage.self) { r in AppStorage( keychain: r.resolve(KeychainSwift.self)!, - userDefaults: r.resolve(UserDefaults.self)!) + userDefaults: r.resolve(UserDefaults.self)! + ) }.inObjectScope(.container) container.register(Validator.self) { _ in diff --git a/NewEdX/DI/ScreenAssembly.swift b/NewEdX/DI/ScreenAssembly.swift index 373311ddc..c1f5514f1 100644 --- a/NewEdX/DI/ScreenAssembly.swift +++ b/NewEdX/DI/ScreenAssembly.swift @@ -201,10 +201,12 @@ class ScreenAssembly: Assembly { } // MARK: CourseScreensView - container - .register(CourseContainerViewModel.self) { r, isActive, courseStart, courseEnd, enrollmentStart, enrollmentEnd in + container.register( + CourseContainerViewModel.self + ) { r, isActive, courseStart, courseEnd, enrollmentStart, enrollmentEnd in CourseContainerViewModel( interactor: r.resolve(CourseInteractorProtocol.self)!, + authInteractor: r.resolve(AuthInteractorProtocol.self)!, router: r.resolve(CourseRouter.self)!, config: r.resolve(Config.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, @@ -217,25 +219,28 @@ class ScreenAssembly: Assembly { ) } - container.register(CourseBlocksViewModel.self) { r, blocks in - CourseBlocksViewModel(blocks: blocks, - manager: r.resolve(DownloadManagerProtocol.self)!, - router: r.resolve(CourseRouter.self)!, - connectivity: r.resolve(ConnectivityProtocol.self)!) - } - - container.register(CourseVerticalViewModel.self) { r, verticals in - CourseVerticalViewModel(verticals: verticals, - manager: r.resolve(DownloadManagerProtocol.self)!, - router: r.resolve(CourseRouter.self)!, - connectivity: r.resolve(ConnectivityProtocol.self)!) + container.register(CourseVerticalViewModel.self) { r, chapters, chapterIndex, sequentialIndex in + CourseVerticalViewModel( + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex, + manager: r.resolve(DownloadManagerProtocol.self)!, + router: r.resolve(CourseRouter.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)! + ) } - container.register(CourseUnitViewModel.self) { r, blockId, courseId, blocks in + container.register( + CourseUnitViewModel.self + ) { r, blockId, courseId, id, chapters, chapterIndex, sequentialIndex, verticalIndex in CourseUnitViewModel( lessonID: blockId, courseID: courseId, - blocks: blocks, + id: id, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex, + verticalIndex: verticalIndex, interactor: r.resolve(CourseInteractorProtocol.self)!, router: r.resolve(CourseRouter.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, @@ -248,18 +253,43 @@ class ScreenAssembly: Assembly { config: r.resolve(Config.self)!) } - container.register(VideoPlayerViewModel.self) { r in - VideoPlayerViewModel(interactor: r.resolve(CourseInteractorProtocol.self)!, - router: r.resolve(CourseRouter.self)!, - connectivity: r.resolve(ConnectivityProtocol.self)!) + container.register( + YouTubeVideoPlayerViewModel.self + ) { r, url, blockID, courseID, languages, playerStateSubject in + YouTubeVideoPlayerViewModel( + url: url, + blockID: blockID, + courseID: courseID, + languages: languages, + playerStateSubject: playerStateSubject, + interactor: r.resolve(CourseInteractorProtocol.self)!, + router: r.resolve(CourseRouter.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)! + ) + } + + container.register( + EncodedVideoPlayerViewModel.self + ) { r, url, blockID, courseID, languages, playerStateSubject in + EncodedVideoPlayerViewModel( + url: url, + blockID: blockID, + courseID: courseID, + languages: languages, + playerStateSubject: playerStateSubject, + interactor: r.resolve(CourseInteractorProtocol.self)!, + router: r.resolve(CourseRouter.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)! + ) } container.register(HandoutsViewModel.self) { r, courseID in - HandoutsViewModel(interactor: r.resolve(CourseInteractorProtocol.self)!, - router: r.resolve(CourseRouter.self)!, - cssInjector: r.resolve(CSSInjector.self)!, - connectivity: r.resolve(ConnectivityProtocol.self)!, - courseID: courseID + HandoutsViewModel( + interactor: r.resolve(CourseInteractorProtocol.self)!, + router: r.resolve(CourseRouter.self)!, + cssInjector: r.resolve(CSSInjector.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)!, + courseID: courseID ) } @@ -272,11 +302,13 @@ class ScreenAssembly: Assembly { router: r.resolve(DiscussionRouter.self)! ) } + container.register(DiscussionInteractorProtocol.self) { r in DiscussionInteractor( repository: r.resolve(DiscussionRepositoryProtocol.self)! ) } + container.register(DiscussionTopicsViewModel.self) { r in DiscussionTopicsViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, @@ -284,6 +316,7 @@ class ScreenAssembly: Assembly { config: r.resolve(Config.self)! ) } + container.register(DiscussionSearchTopicsViewModel.self) { r, courseID in DiscussionSearchTopicsViewModel( courseID: courseID, @@ -292,6 +325,7 @@ class ScreenAssembly: Assembly { debounce: .searchDebounce ) } + container.register(PostsViewModel.self) { r in PostsViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, @@ -299,6 +333,7 @@ class ScreenAssembly: Assembly { config: r.resolve(Config.self)! ) } + container.register(ThreadViewModel.self) { r, subject in ThreadViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, @@ -308,6 +343,7 @@ class ScreenAssembly: Assembly { postStateSubject: subject ) } + container.register(ResponsesViewModel.self) { r, subject in ResponsesViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, @@ -317,6 +353,7 @@ class ScreenAssembly: Assembly { threadStateSubject: subject ) } + container.register(CreateNewThreadViewModel.self) { r in CreateNewThreadViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, diff --git a/NewEdX/Router.swift b/NewEdX/Router.swift index cfb51c2e2..5a9acd4f3 100644 --- a/NewEdX/Router.swift +++ b/NewEdX/Router.swift @@ -18,7 +18,12 @@ import Dashboard import Profile import Combine -public class Router: AuthorizationRouter, DiscoveryRouter, ProfileRouter, DashboardRouter, CourseRouter, DiscussionRouter { +public class Router: AuthorizationRouter, + DiscoveryRouter, + ProfileRouter, + DashboardRouter, + CourseRouter, + DiscussionRouter { public var container: Container @@ -85,19 +90,23 @@ public class Router: AuthorizationRouter, DiscoveryRouter, ProfileRouter, Dashbo public func presentAlert( alertTitle: String, alertMessage: String, + nextSectionName: String? = nil, action: String, image: Image, onCloseTapped: @escaping () -> Void, - okTapped: @escaping () -> Void + okTapped: @escaping () -> Void, + nextSectionTapped: @escaping () -> Void ) { presentView(transitionStyle: .crossDissolve, content: { AlertView( alertTitle: alertTitle, alertMessage: alertMessage, + nextSectionName: nextSectionName, mainAction: action, image: image, onCloseTapped: onCloseTapped, - okTapped: okTapped + okTapped: okTapped, + nextSectionTapped: { nextSectionTapped() } ) }) } @@ -107,8 +116,8 @@ public class Router: AuthorizationRouter, DiscoveryRouter, ProfileRouter, Dashbo } public func presentView(transitionStyle: UIModalTransitionStyle, content: () -> any View) { - navigationController.present(prepareToPresent(content(), - transitionStyle: transitionStyle), animated: true) + let view = prepareToPresent(content(), transitionStyle: transitionStyle) + navigationController.present(view, animated: true) } public func showRegisterScreen() { @@ -123,10 +132,12 @@ public class Router: AuthorizationRouter, DiscoveryRouter, ProfileRouter, Dashbo navigationController.pushViewController(controller, animated: true) } - public func showCourseDetais(courseID: String, - title: String) { - let view = CourseDetailsView(viewModel: Container.shared.resolve(CourseDetailsViewModel.self)!, - courseID: courseID, title: title) + public func showCourseDetais(courseID: String, title: String) { + let view = CourseDetailsView( + viewModel: Container.shared.resolve(CourseDetailsViewModel.self)!, + courseID: courseID, + title: title + ) let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -147,85 +158,150 @@ public class Router: AuthorizationRouter, DiscoveryRouter, ProfileRouter, Dashbo navigationController.pushFade(viewController: controller) } - public func showCourseVerticalView(title: String, - verticals: [CourseVertical]) { - - let viewModel = Container.shared.resolve(CourseVerticalViewModel.self, argument: verticals)! - - let view = CourseVerticalView(title: title, viewModel: viewModel) - let controller = SwiftUIHostController(view: view) - navigationController.pushViewController(controller, animated: true) - } - - public func showCourseBlocksView(title: String, - blocks: [CourseBlock]) { - let viewModel = Container.shared.resolve(CourseBlocksViewModel.self, argument: blocks)! + public func showCourseVerticalView( + id: String, + title: String, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) { + let viewModel = Container.shared.resolve( + CourseVerticalViewModel.self, + arguments: chapters, + chapterIndex, + sequentialIndex + )! - let view = CourseBlocksView(title: title, viewModel: viewModel) + let view = CourseVerticalView(title: title, id: id, viewModel: viewModel) let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } - public func showCourseVerticalAndBlocksView(verticals: (String, [CourseVertical]), - blocks: (String, [CourseBlock])) { - let viewModelVertical = Container.shared.resolve(CourseVerticalViewModel.self, argument: verticals.1)! - let verticalView = CourseVerticalView(title: verticals.0, viewModel: viewModelVertical) - let verticalController = SwiftUIHostController(view: verticalView) - - let viewModelBlocks = Container.shared.resolve(CourseBlocksViewModel.self, argument: blocks.1)! - let blocksView = CourseBlocksView(title: blocks.0, viewModel: viewModelBlocks) - let blocksController = SwiftUIHostController(view: blocksView) - - var currentViews = navigationController.viewControllers - currentViews.append(verticalController) - currentViews.append(blocksController) - - navigationController.setViewControllers(currentViews, animated: true) - } - - public func showCourseScreens(courseID: String, - isActive: Bool?, - courseStart: Date?, - courseEnd: Date?, - enrollmentStart: Date?, - enrollmentEnd: Date?, - title: String) { + public func showCourseScreens( + courseID: String, + isActive: Bool?, + courseStart: Date?, + courseEnd: Date?, + enrollmentStart: Date?, + enrollmentEnd: Date?, + title: String + ) { + let vm = Container.shared.resolve( + CourseContainerViewModel.self, + arguments: isActive, + courseStart, + courseEnd, + enrollmentStart, + enrollmentEnd + )! let screensView = CourseContainerView( - viewModel: Container.shared.resolve(CourseContainerViewModel.self, - arguments: isActive, courseStart, courseEnd, - enrollmentStart, enrollmentEnd)!, + viewModel: vm, courseID: courseID, title: title ) - + let controller = SwiftUIHostController(view: screensView) navigationController.pushViewController(controller, animated: true) } - public func showHandoutsUpdatesView(handouts: String?, - announcements: [CourseUpdate]?, - router: Course.CourseRouter, - cssInjector: CSSInjector) { - let view = HandoutsUpdatesDetailView(handouts: handouts, - announcements: announcements, - router: router, - cssInjector: cssInjector) + public func showHandoutsUpdatesView( + handouts: String?, + announcements: [CourseUpdate]?, + router: Course.CourseRouter, + cssInjector: CSSInjector + ) { + let view = HandoutsUpdatesDetailView( + handouts: handouts, + announcements: announcements, + router: router, + cssInjector: cssInjector + ) let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } - public func showCourseUnit(blockId: String, courseID: String, sectionName: String, blocks: [CourseBlock]) { - let viewModel = Container.shared.resolve(CourseUnitViewModel.self, arguments: blockId, courseID, blocks)! + public func showCourseUnit( + id: String, + blockId: String, + courseID: String, + sectionName: String, + verticalIndex: Int, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) { + let viewModel = Container.shared.resolve( + CourseUnitViewModel.self, + arguments: blockId, + courseID, + id, + chapters, + chapterIndex, + sequentialIndex, + verticalIndex + )! let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName) let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } + public func replaceCourseUnit( + id: String, + blockId: String, + courseID: String, + sectionName: String, + verticalIndex: Int, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) { + + let vmVertical = Container.shared.resolve( + CourseVerticalViewModel.self, + arguments: chapters, + chapterIndex, + sequentialIndex + )! + + let viewVertical = CourseVerticalView( + title: chapters[chapterIndex].childs[sequentialIndex].displayName, + id: id, + viewModel: vmVertical + ) + let controllerVertical = SwiftUIHostController(view: viewVertical) + + let verticals = chapters[chapterIndex].childs[sequentialIndex].childs + + let viewModel = Container.shared.resolve( + CourseUnitViewModel.self, + arguments: blockId, + courseID, + id, + chapters, + chapterIndex, + sequentialIndex, + verticalIndex + )! + let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName) + let controllerUnit = SwiftUIHostController(view: view) + var controllers = navigationController.viewControllers + controllers.removeLast(2) + controllers.append(contentsOf: [controllerVertical, controllerUnit]) + navigationController.setViewControllers(controllers, animated: true) + } + public func showThreads(courseID: String, topics: Topics, title: String, type: ThreadType) { let router = Container.shared.resolve(DiscussionRouter.self)! let viewModel = Container.shared.resolve(PostsViewModel.self)! - let view = PostsView(courseID: courseID, currentBlockID: "", topics: topics, title: title, - type: type, viewModel: viewModel, router: router) + let view = PostsView( + courseID: courseID, + currentBlockID: "", + topics: topics, + title: title, + type: type, + viewModel: viewModel, + router: router + ) let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -244,7 +320,12 @@ public class Router: AuthorizationRouter, DiscoveryRouter, ProfileRouter, Dashbo ) { let router = Container.shared.resolve(DiscussionRouter.self)! let viewModel = Container.shared.resolve(ResponsesViewModel.self, argument: threadStateSubject)! - let view = ResponsesView(commentID: commentID, viewModel: viewModel, router: router, parentComment: parentComment) + let view = ResponsesView( + commentID: commentID, + viewModel: viewModel, + router: router, + parentComment: parentComment + ) let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -255,19 +336,27 @@ public class Router: AuthorizationRouter, DiscoveryRouter, ProfileRouter, Dashbo onPostCreated: @escaping () -> Void ) { let viewModel = Container.shared.resolve(CreateNewThreadViewModel.self)! - let view = CreateNewThreadView(viewModel: viewModel, selectedTopic: selectedTopic, - courseID: courseID, onPostCreated: onPostCreated) + let view = CreateNewThreadView( + viewModel: viewModel, + selectedTopic: selectedTopic, + courseID: courseID, + onPostCreated: onPostCreated + ) let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } - public func showEditProfile(userModel: Core.UserProfile, - avatar: UIImage?, - profileDidEdit: @escaping ((UserProfile?, UIImage?)) -> Void) { + public func showEditProfile( + userModel: Core.UserProfile, + avatar: UIImage?, + profileDidEdit: @escaping ((UserProfile?, UIImage?)) -> Void + ) { let viewModel = Container.shared.resolve(EditProfileViewModel.self, argument: userModel)! - let view = EditProfileView(viewModel: viewModel, - avatar: avatar, - profileDidEdit: profileDidEdit) + let view = EditProfileView( + viewModel: viewModel, + avatar: avatar, + profileDidEdit: profileDidEdit + ) let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -286,9 +375,11 @@ public class Router: AuthorizationRouter, DiscoveryRouter, ProfileRouter, Dashbo } private func present(transitionStyle: UIModalTransitionStyle, view: ToPresent) { - navigationController.present(prepareToPresent(view, transitionStyle: transitionStyle), - animated: true, - completion: {}) + navigationController.present( + prepareToPresent(view, transitionStyle: transitionStyle), + animated: true, + completion: {} + ) } public func showDeleteProfileView() { diff --git a/Podfile b/Podfile index 9166c04f7..119bfab27 100644 --- a/Podfile +++ b/Podfile @@ -24,7 +24,6 @@ abstract_target "App" do pod 'Introspect', '~> 0.6' pod 'Kingfisher', '~> 7.8' pod 'Swinject', '2.8.3' - end target "Authorization" do project './Authorization/Authorization.xcodeproj' diff --git a/Podfile.lock b/Podfile.lock index bcdbf64d2..3b9db589b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,4 +1,5 @@ PODS: + - Alamofire (5.7.1) - Introspect (0.6.1) - KeychainSwift (20.0.0) diff --git a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift index 947e4d9a2..bef8c06c9 100644 --- a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift +++ b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift @@ -10,7 +10,8 @@ import Core public struct DeleteAccountView: View { - @ObservedObject private var viewModel: DeleteAccountViewModel + @ObservedObject + private var viewModel: DeleteAccountViewModel public init(viewModel: DeleteAccountViewModel) { self.viewModel = viewModel @@ -21,8 +22,10 @@ public struct DeleteAccountView: View { // MARK: - Page name VStack(alignment: .center) { - NavigationBar(title: ProfileLocalization.DeleteAccount.title, - leftButtonAction: { viewModel.router.back() }) + NavigationBar( + title: ProfileLocalization.DeleteAccount.title, + leftButtonAction: { viewModel.router.back() } + ) .frameLimit() @@ -149,7 +152,11 @@ public struct DeleteAccountView: View { struct DeleteAccountView_Previews: PreviewProvider { static var previews: some View { let router = ProfileRouterMock() - let vm = DeleteAccountViewModel(interactor: ProfileInteractor.mock, router: router, connectivity: Connectivity()) + let vm = DeleteAccountViewModel( + interactor: ProfileInteractor.mock, + router: router, + connectivity: Connectivity() + ) DeleteAccountView(viewModel: vm) } diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift index 5aa63e83c..0005797aa 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift @@ -16,9 +16,11 @@ public struct EditProfileView: View { private var oldAvatar: UIImage? private var profileDidEdit: ((UserProfile?, UIImage?)) -> Void - public init(viewModel: EditProfileViewModel, - avatar: UIImage?, - profileDidEdit: @escaping ((UserProfile?, UIImage?)) -> Void) { + public init( + viewModel: EditProfileViewModel, + avatar: UIImage?, + profileDidEdit: @escaping ((UserProfile?, UIImage?)) -> Void + ) { self.viewModel = viewModel self.profileDidEdit = profileDidEdit self.viewModel.inputImage = avatar @@ -31,22 +33,26 @@ public struct EditProfileView: View { // MARK: - Page name VStack(alignment: .center) { - NavigationBar(title: ProfileLocalization.editProfile, - leftButtonAction: { - viewModel.backButtonTapped() - if viewModel.profileChanges.isAvatarSaved { - self.profileDidEdit((viewModel.editedProfile, viewModel.inputImage)) - } else { - self.profileDidEdit((viewModel.editedProfile, oldAvatar)) - } - }, rightButtonType: .done, - rightButtonAction: { - if viewModel.isChanged { - Task { - await viewModel.saveProfileUpdates() + NavigationBar( + title: ProfileLocalization.editProfile, + leftButtonAction: { + viewModel.backButtonTapped() + if viewModel.profileChanges.isAvatarSaved { + self.profileDidEdit((viewModel.editedProfile, viewModel.inputImage)) + } else { + self.profileDidEdit((viewModel.editedProfile, oldAvatar)) } - } - }, rightButtonIsActive: $viewModel.isChanged) + }, + rightButtonType: .done, + rightButtonAction: { + if viewModel.isChanged { + Task { + await viewModel.saveProfileUpdates() + } + } + }, + rightButtonIsActive: $viewModel.isChanged + ) // MARK: - Page Body ScrollView { @@ -82,8 +88,10 @@ public struct EditProfileView: View { .font(Theme.Fonts.labelLarge) Group { - PickerView(config: viewModel.yearsConfiguration, - router: viewModel.router) + PickerView( + config: viewModel.yearsConfiguration, + router: viewModel.router + ) if viewModel.isEditable { VStack(alignment: .leading) { PickerView(config: viewModel.countriesConfiguration, diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift index 901874745..e34fbef0c 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift @@ -41,11 +41,14 @@ public class EditProfileViewModel: ObservableObject { ProfileLocalization.Edit.Fields.spokenLangugae ) - @Published public var profileChanges: Changes = .init(shortBiography: "", - profileType: .limited, - isAvatarChanged: false, - isAvatarDeleted: false, - isAvatarSaved: false) + @Published + public var profileChanges: Changes = .init( + shortBiography: "", + profileType: .limited, + isAvatarChanged: false, + isAvatarDeleted: false, + isAvatarSaved: false + ) @Published public var inputImage: UIImage? private(set) var isYongUser: Bool = false @@ -73,7 +76,7 @@ public class EditProfileViewModel: ObservableObject { } private let interactor: ProfileInteractorProtocol - public let router: ProfileRouter + let router: ProfileRouter public init(userModel: UserProfile, interactor: ProfileInteractorProtocol, router: ProfileRouter) { self.userModel = userModel @@ -84,7 +87,7 @@ public class EditProfileViewModel: ObservableObject { generateYears() } - public func resizeImage(image: UIImage, longSideSize: Double) { + func resizeImage(image: UIImage, longSideSize: Double) { let size = image.size let widthRatio = longSideSize / size.width @@ -107,7 +110,7 @@ public class EditProfileViewModel: ObservableObject { } @MainActor - public func deleteAvatar() async throws { + func deleteAvatar() async throws { isShowProgress = true do { if try await interactor.deleteProfilePicture() { @@ -124,7 +127,7 @@ public class EditProfileViewModel: ObservableObject { } } - public func checkChanges() { + func checkChanges() { withAnimation(.easeIn(duration: 0.1)) { self.isChanged = [spokenLanguageConfiguration.text.isEmpty ? false : spokenLanguageConfiguration.text != userModel.spokenLanguage, @@ -137,7 +140,7 @@ public class EditProfileViewModel: ObservableObject { } } - public func switchProfile() { + func switchProfile() { var yearOfBirth = 0 if yearsConfiguration.text != "" { yearOfBirth = Int(yearsConfiguration.text) ?? 0 @@ -151,7 +154,7 @@ public class EditProfileViewModel: ObservableObject { } } - public func checkProfileType() { + func checkProfileType() { if yearsConfiguration.text != "" { let yearOfBirth = yearsConfiguration.text if currentYear - (Int(yearOfBirth) ?? 0) < 13 { @@ -180,7 +183,7 @@ public class EditProfileViewModel: ObservableObject { } @MainActor - public func saveProfileUpdates() async { + func saveProfileUpdates() async { var parameters: [String: Any] = [:] if userModel.isFullProfile != profileChanges.profileType.boolValue { @@ -206,7 +209,7 @@ public class EditProfileViewModel: ObservableObject { } @MainActor - private func uploadData(parameters: [String: Any]) async { + func uploadData(parameters: [String: Any]) async { do { if profileChanges.isAvatarDeleted { try await deleteAvatar() @@ -248,7 +251,7 @@ public class EditProfileViewModel: ObservableObject { } } - public func backButtonTapped() { + func backButtonTapped() { if isChanged { router.presentAlert( alertTitle: ProfileLocalization.UnsavedDataAlert.title, @@ -268,6 +271,26 @@ public class EditProfileViewModel: ObservableObject { } } + func loadLocationsAndSpokenLanguages() { + if let yearOfBirth = userModel.yearOfBirth == 0 ? nil : userModel.yearOfBirth { + self.selectedYearOfBirth = PickerItem(key: "\(yearOfBirth)", value: "\(yearOfBirth)") + } + + if let index = countries.firstIndex(where: {$0.value == userModel.country}) { + countries[index].optionDefault = true + let selected = countries[index] + self.selectedCountry = PickerItem(key: selected.value, value: selected.name) + } + if let spokenLanguage = userModel.spokenLanguage { + if let spokenIndex = spokenLanguages.firstIndex(where: {$0.value == spokenLanguage }) { + let selected = spokenLanguages[spokenIndex] + self.selectedSpokeLanguage = PickerItem(key: selected.value, value: selected.name) + } + } + + generateFieldConfigurations() + } + private func generateYears() { let currentYear = Calendar.current.component(.year, from: Date()) years = [] @@ -311,26 +334,6 @@ public class EditProfileViewModel: ObservableObject { options: spokenLanguages), selectedItem: selectedSpokeLanguage) - profileChanges.shortBiography = userModel.shortBiography ?? "" - } - - public func loadLocationsAndSpokenLanguages() { - if let yearOfBirth = userModel.yearOfBirth == 0 ? nil : userModel.yearOfBirth { - self.selectedYearOfBirth = PickerItem(key: "\(yearOfBirth)", value: "\(yearOfBirth)") - } - - if let index = countries.firstIndex(where: {$0.value == userModel.country}) { - countries[index].optionDefault = true - let selected = countries[index] - self.selectedCountry = PickerItem(key: selected.value, value: selected.name) - } - if let spokenLanguage = userModel.spokenLanguage { - if let spokenIndex = spokenLanguages.firstIndex(where: {$0.value == spokenLanguage }) { - let selected = spokenLanguages[spokenIndex] - self.selectedSpokeLanguage = PickerItem(key: selected.value, value: selected.name) - } - } - - generateFieldConfigurations() + profileChanges.shortBiography = userModel.shortBiography } } diff --git a/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift b/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift index 08cd68df2..79f100c2c 100644 --- a/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift +++ b/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift @@ -38,9 +38,11 @@ struct ProfileBottomSheet: View { private var removePhoto: () -> Void @Binding private var showingBottomSheet: Bool - init(showingBottomSheet: Binding, - openGallery: @escaping () -> Void, - removePhoto: @escaping () -> Void) { + init( + showingBottomSheet: Binding, + openGallery: @escaping () -> Void, + removePhoto: @escaping () -> Void + ) { self._showingBottomSheet = showingBottomSheet self.openGallery = openGallery self.removePhoto = removePhoto diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index d9031b13a..1d3f9baf5 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -26,8 +26,8 @@ public struct ProfileView: View { // MARK: - Page name VStack(alignment: .center) { NavigationBar(title: ProfileLocalization.title, - rightButtonType: .edit, - rightButtonAction: { + rightButtonType: .edit, + rightButtonAction: { if let userModel = viewModel.userModel { viewModel.router.showEditProfile( userModel: userModel, @@ -43,7 +43,7 @@ public struct ProfileView: View { ) } }, rightButtonIsActive: .constant(viewModel.connectivity.isInternetAvaliable)) - + // MARK: - Page Body RefreshableScrollViewCompat(action: { @@ -90,8 +90,10 @@ public struct ProfileView: View { } } } - .cardStyle(bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor, - strokeColor: .clear) + .cardStyle( + bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor, + strokeColor: .clear + ) }.padding(.bottom, 16) } @@ -102,16 +104,18 @@ public struct ProfileView: View { .font(Theme.Fonts.labelLarge) VStack(alignment: .leading, spacing: 27) { HStack { - Button(action: { - viewModel.router.showSettings() - }, label: { + Button(action: { + viewModel.router.showSettings() + }, label: { Text(ProfileLocalization.settingsVideo) Spacer() Image(systemName: "chevron.right") }) } - }.cardStyle(bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor, - strokeColor: .clear) + }.cardStyle( + bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor, + strokeColor: .clear + ) // MARK: - Support info Text(ProfileLocalization.supportInfo) @@ -119,7 +123,7 @@ public struct ProfileView: View { .font(Theme.Fonts.labelLarge) VStack(alignment: .leading, spacing: 24) { if let support = viewModel.contactSupport() { - HStack { + HStack { Link(destination: support, label: { Text(ProfileLocalization.contact) Spacer() @@ -151,8 +155,10 @@ public struct ProfileView: View { }) } } - }.cardStyle(bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor, - strokeColor: .clear) + }.cardStyle( + bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor, + strokeColor: .clear + ) // MARK: - Log out VStack { diff --git a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift index a489c7afe..a8640cf51 100644 --- a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift +++ b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift @@ -24,9 +24,9 @@ public class ProfileViewModel: ObservableObject { } private let interactor: ProfileInteractorProtocol - public let router: ProfileRouter - public let config: Config - public let connectivity: ConnectivityProtocol + let router: ProfileRouter + let config: Config + let connectivity: ConnectivityProtocol public init(interactor: ProfileInteractorProtocol, router: ProfileRouter, diff --git a/Profile/Profile/Presentation/ProfileRouter.swift b/Profile/Profile/Presentation/ProfileRouter.swift index 659b15205..38f0de7e4 100644 --- a/Profile/Profile/Presentation/ProfileRouter.swift +++ b/Profile/Profile/Presentation/ProfileRouter.swift @@ -12,8 +12,11 @@ import UIKit //sourcery: AutoMockable public protocol ProfileRouter: BaseRouter { - func showEditProfile(userModel: Core.UserProfile, avatar: UIImage?, - profileDidEdit: @escaping ((UserProfile?, UIImage?)) -> Void) + func showEditProfile( + userModel: Core.UserProfile, + avatar: UIImage?, + profileDidEdit: @escaping ((UserProfile?, UIImage?)) -> Void + ) func showSettings() @@ -29,8 +32,11 @@ public class ProfileRouterMock: BaseRouterMock, ProfileRouter { public override init() {} - public func showEditProfile(userModel: Core.UserProfile, avatar: UIImage?, - profileDidEdit: @escaping ((UserProfile?, UIImage?)) -> Void) {} + public func showEditProfile( + userModel: Core.UserProfile, + avatar: UIImage?, + profileDidEdit: @escaping ((UserProfile?, UIImage?)) -> Void + ) {} public func showSettings() {} diff --git a/Profile/Profile/Presentation/Settings/SettingsView.swift b/Profile/Profile/Presentation/Settings/SettingsView.swift index a9a85662a..84b6869af 100644 --- a/Profile/Profile/Presentation/Settings/SettingsView.swift +++ b/Profile/Profile/Presentation/Settings/SettingsView.swift @@ -11,11 +11,11 @@ import Kingfisher public struct SettingsView: View { - @ObservedObject private var viewModel: SettingsViewModel + @ObservedObject + private var viewModel: SettingsViewModel public init(viewModel: SettingsViewModel) { self.viewModel = viewModel - } public var body: some View { @@ -24,7 +24,7 @@ public struct SettingsView: View { // MARK: - Page name VStack(alignment: .center) { NavigationBar(title: ProfileLocalization.Settings.videoSettingsTitle, - leftButtonAction: { viewModel.router.back() }) + leftButtonAction: { viewModel.router.back() }) // MARK: - Page Body @@ -35,11 +35,12 @@ public struct SettingsView: View { .padding(.top, 200) .padding(.horizontal) } else { - // MARK: Wi-fi HStack { - SettingsCell(title: ProfileLocalization.Settings.wifiTitle, - description: ProfileLocalization.Settings.wifiDescription) + SettingsCell( + title: ProfileLocalization.Settings.wifiTitle, + description: ProfileLocalization.Settings.wifiDescription + ) Toggle(isOn: $viewModel.wifiOnly, label: {}) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .frame(width: 50) @@ -54,7 +55,7 @@ public struct SettingsView: View { SettingsCell(title: ProfileLocalization.Settings.videoQualityTitle, description: viewModel.selectedQuality.settingsDescription()) }) -// Spacer() + // Spacer() Image(systemName: "chevron.right") .padding(.trailing, 12) .frame(width: 10) @@ -93,8 +94,10 @@ public struct SettingsView: View { struct SettingsView_Previews: PreviewProvider { static var previews: some View { let router = ProfileRouterMock() - let vm = SettingsViewModel(interactor: ProfileInteractor.mock, - router: router) + let vm = SettingsViewModel( + interactor: ProfileInteractor.mock, + router: router + ) SettingsView(viewModel: vm) .preferredColorScheme(.light) diff --git a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift index 8973f63cf..99e11ba38 100644 --- a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift +++ b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift @@ -43,7 +43,7 @@ public class SettingsViewModel: ObservableObject { private var userSettings: UserSettings private let interactor: ProfileInteractorProtocol - public let router: ProfileRouter + let router: ProfileRouter public init(interactor: ProfileInteractorProtocol, router: ProfileRouter) { self.interactor = interactor diff --git a/Profile/Profile/Presentation/Settings/VideoQualityView.swift b/Profile/Profile/Presentation/Settings/VideoQualityView.swift index 1e2a0905e..bcf071d6a 100644 --- a/Profile/Profile/Presentation/Settings/VideoQualityView.swift +++ b/Profile/Profile/Presentation/Settings/VideoQualityView.swift @@ -11,11 +11,11 @@ import Kingfisher public struct VideoQualityView: View { - @ObservedObject private var viewModel: SettingsViewModel + @ObservedObject + private var viewModel: SettingsViewModel public init(viewModel: SettingsViewModel) { self.viewModel = viewModel - } public var body: some View { @@ -24,7 +24,7 @@ public struct VideoQualityView: View { // MARK: - Page name VStack(alignment: .center) { NavigationBar(title: ProfileLocalization.Settings.videoQualityTitle, - leftButtonAction: { viewModel.router.back() }) + leftButtonAction: { viewModel.router.back() }) // MARK: - Page Body @@ -40,20 +40,22 @@ public struct VideoQualityView: View { Button(action: { viewModel.selectedQuality = quality }, label: { - HStack { - SettingsCell(title: quality.title(), - description: quality.description()) - Spacer() + HStack { + SettingsCell( + title: quality.title(), + description: quality.description() + ) + Spacer() CoreAssets.checkmark.swiftUIImage .renderingMode(.template) .foregroundColor(.accentColor) .opacity(quality == viewModel.selectedQuality ? 1 : 0) - - }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) + + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) }) Divider() } - + } }.frame(minWidth: 0, maxWidth: .infinity, diff --git a/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift index cefabff3f..c5f1e63ee 100644 --- a/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift @@ -390,7 +390,7 @@ final class EditProfileViewModelTests: XCTestCase { viewModel.checkProfileType() XCTAssertEqual(viewModel.profileChanges.profileType, .limited) - XCTAssertTrue(viewModel.isYongUser) + XCTAssertFalse(viewModel.isYongUser) XCTAssertFalse(viewModel.isEditable) XCTAssertTrue(viewModel.profileChanges.profileType == .limited) } diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index c41a29771..a13760a6f 100644 --- a/Profile/ProfileTests/ProfileMock.generated.swift +++ b/Profile/ProfileTests/ProfileMock.generated.swift @@ -514,10 +514,10 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`) } - open func presentAlert(alertTitle: String, alertMessage: String, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void) { - addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) - let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) as? (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void - perform?(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`) + open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void + perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`) } open func presentView(transitionStyle: UIModalTransitionStyle, view: any View) { @@ -544,7 +544,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showRegisterScreen case m_showForgotPasswordScreen case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) - case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) case m_presentView__transitionStyle_transitionStylecontent_content(Parameter, Parameter<() -> any View>) @@ -590,14 +590,16 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) return Matcher.ComparisonResult(results) - case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped)): + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsNextsectionname, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped, let lhsNextsectiontapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsNextsectionname, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped, let rhsNextsectiontapped)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectionname, rhs: rhsNextsectionname, with: matcher), lhsNextsectionname, rhsNextsectionname, "nextSectionName")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsImage, rhs: rhsImage, with: matcher), lhsImage, rhsImage, "image")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectiontapped, rhs: rhsNextsectiontapped, with: matcher), lhsNextsectiontapped, rhsNextsectiontapped, "nextSectionTapped")) return Matcher.ComparisonResult(results) case (.m_presentView__transitionStyle_transitionStyleview_view(let lhsTransitionstyle, let lhsView), .m_presentView__transitionStyle_transitionStyleview_view(let rhsTransitionstyle, let rhsView)): @@ -627,7 +629,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return 0 case .m_showForgotPasswordScreen: return 0 case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue - case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue case let .m_presentView__transitionStyle_transitionStylecontent_content(p0, p1): return p0.intValue + p1.intValue } @@ -644,7 +646,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showRegisterScreen: return ".showRegisterScreen()" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" - case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped: return ".presentAlert(alertTitle:alertMessage:action:image:onCloseTapped:okTapped:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" case .m_presentView__transitionStyle_transitionStylecontent_content: return ".presentView(transitionStyle:content:)" } @@ -675,7 +677,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} public static func presentView(transitionStyle: Parameter, content: Parameter<() -> any View>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStylecontent_content(`transitionStyle`, `content`))} } @@ -714,8 +716,8 @@ open class BaseRouterMock: BaseRouter, Mock { public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, perform: @escaping (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { - return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`), performs: perform) + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>, perform: @escaping (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`), performs: perform) } public static func presentView(transitionStyle: Parameter, view: Parameter, perform: @escaping (UIModalTransitionStyle, any View) -> Void) -> Perform { return Perform(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`), performs: perform) @@ -1659,10 +1661,10 @@ open class ProfileRouterMock: ProfileRouter, Mock { perform?(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`) } - open func presentAlert(alertTitle: String, alertMessage: String, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void) { - addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) - let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`))) as? (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void - perform?(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`) + open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void + perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`) } open func presentView(transitionStyle: UIModalTransitionStyle, view: any View) { @@ -1693,7 +1695,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { case m_showRegisterScreen case m_showForgotPasswordScreen case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) - case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) case m_presentView__transitionStyle_transitionStylecontent_content(Parameter, Parameter<() -> any View>) @@ -1755,14 +1757,16 @@ open class ProfileRouterMock: ProfileRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) return Matcher.ComparisonResult(results) - case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped)): + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsNextsectionname, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped, let lhsNextsectiontapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsNextsectionname, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped, let rhsNextsectiontapped)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectionname, rhs: rhsNextsectionname, with: matcher), lhsNextsectionname, rhsNextsectionname, "nextSectionName")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsImage, rhs: rhsImage, with: matcher), lhsImage, rhsImage, "image")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectiontapped, rhs: rhsNextsectiontapped, with: matcher), lhsNextsectiontapped, rhsNextsectiontapped, "nextSectionTapped")) return Matcher.ComparisonResult(results) case (.m_presentView__transitionStyle_transitionStyleview_view(let lhsTransitionstyle, let lhsView), .m_presentView__transitionStyle_transitionStyleview_view(let rhsTransitionstyle, let rhsView)): @@ -1796,7 +1800,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { case .m_showRegisterScreen: return 0 case .m_showForgotPasswordScreen: return 0 case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue - case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue case let .m_presentView__transitionStyle_transitionStylecontent_content(p0, p1): return p0.intValue + p1.intValue } @@ -1817,7 +1821,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { case .m_showRegisterScreen: return ".showRegisterScreen()" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" - case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped: return ".presentAlert(alertTitle:alertMessage:action:image:onCloseTapped:okTapped:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" case .m_presentView__transitionStyle_transitionStylecontent_content: return ".presentView(transitionStyle:content:)" } @@ -1852,7 +1856,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} public static func presentView(transitionStyle: Parameter, content: Parameter<() -> any View>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStylecontent_content(`transitionStyle`, `content`))} } @@ -1903,8 +1907,8 @@ open class ProfileRouterMock: ProfileRouter, Mock { public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } - public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, perform: @escaping (String, String, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { - return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessageaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTapped(`alertTitle`, `alertMessage`, `action`, `image`, `onCloseTapped`, `okTapped`), performs: perform) + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>, perform: @escaping (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`), performs: perform) } public static func presentView(transitionStyle: Parameter, view: Parameter, perform: @escaping (UIModalTransitionStyle, any View) -> Void) -> Perform { return Perform(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`), performs: perform) From 624f94d3b42b73c33846d66659cc18ac3b952aa5 Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:11:15 +0300 Subject: [PATCH 20/26] fix translate (#41) --- Course/Course/Data/CourseRepository.swift | 30 +++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index 30f3555df..f042b9792 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -409,7 +409,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78", "type": "comparison", - "display_name": "Співставлення", + "display_name": "Comparison", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -445,7 +445,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b", "type": "comparison", - "display_name": "Співставлення", + "display_name": "Comparison", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -535,7 +535,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7", "type": "comparison", - "display_name": "Співставлення", + "display_name": "Comparison", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -553,7 +553,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904", "type": "vertical", - "display_name": "Юніт", + "display_name": "Unit", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -578,7 +578,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6", "type": "sequential", - "display_name": "Підрозділ", + "display_name": "Subsection", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -657,7 +657,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca", "type": "vertical", - "display_name": "Юніт", + "display_name": "Unit", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -677,7 +677,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2", "type": "sequential", - "display_name": "Ще один Підрозділ", + "display_name": "Another one subsection", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -695,7 +695,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846", "type": "chapter", - "display_name": "Розділ", + "display_name": "Section", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -714,7 +714,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de", "type": "pdf", - "display_name": "PDF файл заголовок", + "display_name": "PDF title", "graded": false, "student_view_data": { "last_modified": "2023-04-26T08:43:45Z", @@ -824,7 +824,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1", "type": "vertical", - "display_name": "Юніт", + "display_name": "Unit", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -864,7 +864,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4", "type": "vertical", - "display_name": "Юніт", + "display_name": "Unit", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -904,7 +904,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1", "type": "vertical", - "display_name": "Юніт", + "display_name": "Unit", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -922,7 +922,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe", "type": "sequential", - "display_name": "Підрозділ", + "display_name": "Subsection", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -960,7 +960,7 @@ And there are various ways of describing it-- call it oral poetry or "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf?experience=legacy", "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf", "type": "chapter", - "display_name": "Розділ", + "display_name": "Section", "graded": false, "student_view_multi_device": false, "block_counts": { @@ -996,7 +996,7 @@ And there are various ways of describing it-- call it oral poetry or "number": "comparison", "org": "QA", "start": "2022-01-01T00:00:00Z", - "start_display": "01 січня 2022 р.", + "start_display": "01 january 2022 р.", "start_type": "timestamp", "end": null, "courseware_access": { From e53baf65d7491afa998a1db9388930c01572e790 Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:22:04 +0300 Subject: [PATCH 21/26] fix pods (#42) --- NewEdX.xcodeproj/project.pbxproj | 48 ++++++++++++++++---------------- Podfile | 1 + Podfile.lock | 9 +++--- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/NewEdX.xcodeproj/project.pbxproj b/NewEdX.xcodeproj/project.pbxproj index 31c72814e..e102a8e84 100644 --- a/NewEdX.xcodeproj/project.pbxproj +++ b/NewEdX.xcodeproj/project.pbxproj @@ -36,7 +36,7 @@ 07D5DA3528D075AA00752FD9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */; }; 07D5DA3E28D075AB00752FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */; }; 07D5DA4128D075AB00752FD9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3F28D075AB00752FD9 /* LaunchScreen.storyboard */; }; - 955D45D9B3C2A224A5869AD6 /* Pods_App_NewEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9840FE925678B7723ACA7167 /* Pods_App_NewEdX.framework */; }; + F9D8E3EA58261028C3968180 /* Pods_App_NewEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16B61E47E3D4524ED0463D63 /* Pods_App_NewEdX.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -91,13 +91,13 @@ 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 07D5DA4028D075AB00752FD9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 40A7E74C7E8BA16CF1C37A27 /* Pods-App-NewEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.releasestage.xcconfig"; sourceTree = ""; }; - 629FEC6E87F0A0CE4EF3BFE3 /* Pods-App-NewEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.debugprod.xcconfig"; sourceTree = ""; }; - 6C40659E4A0D422E211C112C /* Pods-App-NewEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.releaseprod.xcconfig"; sourceTree = ""; }; - 7C063BCA6EAA90159BF3AEE0 /* Pods-App-NewEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.debugdev.xcconfig"; sourceTree = ""; }; - 850717CD110D52547BED165B /* Pods-App-NewEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.releasedev.xcconfig"; sourceTree = ""; }; - 9840FE925678B7723ACA7167 /* Pods_App_NewEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_NewEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - E7DE8FAB4E16DE50EDE7A5BF /* Pods-App-NewEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.debugstage.xcconfig"; sourceTree = ""; }; + 16B61E47E3D4524ED0463D63 /* Pods_App_NewEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_NewEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 31F7D13F89ABEA0C978F4988 /* Pods-App-NewEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.debugstage.xcconfig"; sourceTree = ""; }; + 92E99692D520163574381814 /* Pods-App-NewEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.releasestage.xcconfig"; sourceTree = ""; }; + B65009A9300CA1C0AA1ADA13 /* Pods-App-NewEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.debugprod.xcconfig"; sourceTree = ""; }; + B7A07A23F1B105D41A41426C /* Pods-App-NewEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.debugdev.xcconfig"; sourceTree = ""; }; + DAFCB10EBCC10B9CE95D5140 /* Pods-App-NewEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.releasedev.xcconfig"; sourceTree = ""; }; + E8680E764DC8A25C24EA74D5 /* Pods-App-NewEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.releaseprod.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -112,7 +112,7 @@ 0218196428F734FA00202564 /* Discussion.framework in Frameworks */, 0219C67728F4347600D64452 /* Course.framework in Frameworks */, 027DB33028D8A063002B6862 /* Dashboard.framework in Frameworks */, - 955D45D9B3C2A224A5869AD6 /* Pods_App_NewEdX.framework in Frameworks */, + F9D8E3EA58261028C3968180 /* Pods_App_NewEdX.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -190,7 +190,7 @@ 072787B028D34D83002E9142 /* Discovery.framework */, 0770DE4A28D0A462006D8A5D /* Authorization.framework */, 0770DE1228D07845006D8A5D /* Core.framework */, - 9840FE925678B7723ACA7167 /* Pods_App_NewEdX.framework */, + 16B61E47E3D4524ED0463D63 /* Pods_App_NewEdX.framework */, ); name = Frameworks; sourceTree = ""; @@ -198,12 +198,12 @@ 55A895025FB07897BA68E063 /* Pods */ = { isa = PBXGroup; children = ( - 629FEC6E87F0A0CE4EF3BFE3 /* Pods-App-NewEdX.debugprod.xcconfig */, - 7C063BCA6EAA90159BF3AEE0 /* Pods-App-NewEdX.debugdev.xcconfig */, - 6C40659E4A0D422E211C112C /* Pods-App-NewEdX.releaseprod.xcconfig */, - 850717CD110D52547BED165B /* Pods-App-NewEdX.releasedev.xcconfig */, - E7DE8FAB4E16DE50EDE7A5BF /* Pods-App-NewEdX.debugstage.xcconfig */, - 40A7E74C7E8BA16CF1C37A27 /* Pods-App-NewEdX.releasestage.xcconfig */, + B65009A9300CA1C0AA1ADA13 /* Pods-App-NewEdX.debugprod.xcconfig */, + 31F7D13F89ABEA0C978F4988 /* Pods-App-NewEdX.debugstage.xcconfig */, + B7A07A23F1B105D41A41426C /* Pods-App-NewEdX.debugdev.xcconfig */, + E8680E764DC8A25C24EA74D5 /* Pods-App-NewEdX.releaseprod.xcconfig */, + 92E99692D520163574381814 /* Pods-App-NewEdX.releasestage.xcconfig */, + DAFCB10EBCC10B9CE95D5140 /* Pods-App-NewEdX.releasedev.xcconfig */, ); path = Pods; sourceTree = ""; @@ -215,7 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 07D5DA4528D075AB00752FD9 /* Build configuration list for PBXNativeTarget "NewEdX" */; buildPhases = ( - 8739D71CE4167C18E475C4E7 /* [CP] Check Pods Manifest.lock */, + 46EB6B53644A79B05619EEAD /* [CP] Check Pods Manifest.lock */, 0770DE2328D08647006D8A5D /* SwiftLint */, 07D5DA2D28D075AA00752FD9 /* Sources */, 07D5DA2E28D075AA00752FD9 /* Frameworks */, @@ -299,7 +299,7 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; - 8739D71CE4167C18E475C4E7 /* [CP] Check Pods Manifest.lock */ = { + 46EB6B53644A79B05619EEAD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -444,7 +444,7 @@ }; 02DD1C9629E80CC200F35DCE /* DebugStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E7DE8FAB4E16DE50EDE7A5BF /* Pods-App-NewEdX.debugstage.xcconfig */; + baseConfigurationReference = 31F7D13F89ABEA0C978F4988 /* Pods-App-NewEdX.debugstage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -533,7 +533,7 @@ }; 02DD1C9829E80CCB00F35DCE /* ReleaseStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 40A7E74C7E8BA16CF1C37A27 /* Pods-App-NewEdX.releasestage.xcconfig */; + baseConfigurationReference = 92E99692D520163574381814 /* Pods-App-NewEdX.releasestage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -628,7 +628,7 @@ }; 0727875928D231FD002E9142 /* DebugDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7C063BCA6EAA90159BF3AEE0 /* Pods-App-NewEdX.debugdev.xcconfig */; + baseConfigurationReference = B7A07A23F1B105D41A41426C /* Pods-App-NewEdX.debugdev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -717,7 +717,7 @@ }; 0727875B28D23204002E9142 /* ReleaseDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 850717CD110D52547BED165B /* Pods-App-NewEdX.releasedev.xcconfig */; + baseConfigurationReference = DAFCB10EBCC10B9CE95D5140 /* Pods-App-NewEdX.releasedev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -866,7 +866,7 @@ }; 07D5DA4628D075AB00752FD9 /* DebugProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 629FEC6E87F0A0CE4EF3BFE3 /* Pods-App-NewEdX.debugprod.xcconfig */; + baseConfigurationReference = B65009A9300CA1C0AA1ADA13 /* Pods-App-NewEdX.debugprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -901,7 +901,7 @@ }; 07D5DA4728D075AB00752FD9 /* ReleaseProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6C40659E4A0D422E211C112C /* Pods-App-NewEdX.releaseprod.xcconfig */; + baseConfigurationReference = E8680E764DC8A25C24EA74D5 /* Pods-App-NewEdX.releaseprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; diff --git a/Podfile b/Podfile index 119bfab27..9166c04f7 100644 --- a/Podfile +++ b/Podfile @@ -24,6 +24,7 @@ abstract_target "App" do pod 'Introspect', '~> 0.6' pod 'Kingfisher', '~> 7.8' pod 'Swinject', '2.8.3' + end target "Authorization" do project './Authorization/Authorization.xcodeproj' diff --git a/Podfile.lock b/Podfile.lock index 3b9db589b..17bcef55a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,9 +1,8 @@ PODS: - - Alamofire (5.7.1) - - Introspect (0.6.1) + - Introspect (0.6.2) - KeychainSwift (20.0.0) - - Kingfisher (7.8.0) + - Kingfisher (7.8.1) - Sourcery (1.8.0): - Sourcery/CLI-Only (= 1.8.0) - Sourcery/CLI-Only (1.8.0) @@ -46,9 +45,9 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: Alamofire: 0123a34370cb170936ae79a8df46cc62b2edeb88 - Introspect: 1b66ef0782311ff003007732e2d940c69545a7be + Introspect: f80afac3cf8ff466700413368a5a2339144f71ce KeychainSwift: 0ce6a4d13f7228054d1a71bb1b500448fb2ab837 - Kingfisher: 0656e1b064bfc1ca1cd04e033f617a86559696e9 + Kingfisher: 63f677311d36a3473f6b978584f8a3845d023dc5 Sourcery: 6f5fe49b82b7e02e8c65560cbd52e1be67a1af2e SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c SwiftLint: 1ac76dac888ca05cb0cf24d0c85887ec1209961d From 6e2c175f3aece4fea7b28814516801f74db2a06b Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta Date: Fri, 23 Jun 2023 22:44:25 +0300 Subject: [PATCH 22/26] Fix DateFormatter locale --- Core/Core/Extensions/DateExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index 5363bc31e..45ddbcb01 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -72,7 +72,7 @@ public enum DateStringStyle { public extension Date { func dateToString(style: DateStringStyle) -> String { let dateFormatter = DateFormatter() - dateFormatter.locale = .current + dateFormatter.locale = Locale(identifier: "en_US_POSIX") switch style { case .endedMonthDay: From ac6018b98cf5a51ea6af4a8de7764021b5ceb93f Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Fri, 30 Jun 2023 13:23:59 +0300 Subject: [PATCH 23/26] Fix linter warnings. (#43) Beautified some code to align with the code style. Fixed Swiftlint 0.51 warning about superfluous disable. --- Core/Core/Configuration/CSSInjector.swift | 2 + Core/Core/Extensions/DateExtension.swift | 24 ++++----- Core/Core/Theme.swift | 1 + Core/Core/View/Base/AlertView.swift | 30 +++++------ Core/Core/View/Base/CourseCellView.swift | 1 + Core/Core/View/Base/TextWithUrls.swift | 1 + .../Handouts/HandoutsUpdatesDetailView.swift | 2 + .../Presentation/Unit/CourseUnitView.swift | 1 + .../DiscussionSearchTopicsView.swift | 5 +- .../DiscussionTopicsView.swift | 1 + .../Presentation/Posts/PostsView.swift | 1 - NewEdX/DI/AppAssembly.swift | 1 + NewEdX/DI/ScreenAssembly.swift | 3 +- NewEdX/StringExtension.swift | 52 ------------------- Profile/Profile/Data/ProfileRepository.swift | 2 + 15 files changed, 42 insertions(+), 85 deletions(-) delete mode 100644 NewEdX/StringExtension.swift diff --git a/Core/Core/Configuration/CSSInjector.swift b/Core/Core/Configuration/CSSInjector.swift index 2101920d1..9e7faf0ec 100644 --- a/Core/Core/Configuration/CSSInjector.swift +++ b/Core/Core/Configuration/CSSInjector.swift @@ -71,6 +71,7 @@ public class CSSInjector { } } + //swiftlint:disable function_body_length line_length public func injectCSS( colorScheme: ColorScheme, html: String, @@ -138,6 +139,7 @@ public class CSSInjector { """ return style + replacedHTML } + //swiftlint:enable function_body_length line_length } diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index 45ddbcb01..fae6cd14c 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -33,18 +33,18 @@ public extension Date { } init(subtitleTime: String) { - let calendar = Calendar.current - let now = Date() - var components = calendar.dateComponents([.year, .month, .day], from: now) - var dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss,SSS" - let dateString = "\(components.year!)-\(components.month!)-\(components.day!) \(subtitleTime)" - guard let date = dateFormatter.date(from: dateString) else { - self = now - return - } - self = date - } + let calendar = Calendar.current + let now = Date() + let components = calendar.dateComponents([.year, .month, .day], from: now) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss,SSS" + let dateString = "\(components.year!)-\(components.month!)-\(components.day!) \(subtitleTime)" + guard let date = dateFormatter.date(from: dateString) else { + self = now + return + } + self = date + } init(milliseconds: Double) { let now = Date() diff --git a/Core/Core/Theme.swift b/Core/Core/Theme.swift index d8eb84573..56db63501 100644 --- a/Core/Core/Theme.swift +++ b/Core/Core/Theme.swift @@ -66,6 +66,7 @@ public extension Theme.Fonts { guard let url = Bundle(for: __.self).url(forResource: "SF-Pro", withExtension: "ttf") else { return } CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil) } + // swiftlint:enable type_name } extension View { diff --git a/Core/Core/View/Base/AlertView.swift b/Core/Core/View/Base/AlertView.swift index d11ec70db..e4dd061a2 100644 --- a/Core/Core/View/Base/AlertView.swift +++ b/Core/Core/View/Base/AlertView.swift @@ -117,7 +117,7 @@ public struct AlertView: View { .saturation(0) case let .action(action, _): VStack(spacing: 20) { - if let nextSectionName { + if nextSectionName != nil { UnitButtonView(type: .nextSection, action: { nextSectionTapped() }) .frame(maxWidth: 215) } @@ -260,24 +260,18 @@ public struct AlertView: View { // swiftlint:disable all struct AlertView_Previews: PreviewProvider { static var previews: some View { -// AlertView( -// alertTitle: "Warning!", -// alertMessage: "Something goes wrong. Do you want to exterminate your phone, right now", -// positiveAction: "Accept", -// onCloseTapped: {}, -// okTapped: {}, -// type: .action("", CoreAssets.goodWork.swiftUIImage) -// ) - AlertView(alertTitle: "Warning", - alertMessage: "Something goes wrong. Do you want to exterminate your phone, right now", - nextSectionName: "Ahmad tea is a power", - mainAction: "Back to outline", - image: CoreAssets.goodWork.swiftUIImage, - onCloseTapped: {}, - okTapped: {}, - nextSectionTapped: {}) - + AlertView( + alertTitle: "Warning", + alertMessage: "Something goes wrong. Do you want to exterminate your phone, right now", + nextSectionName: "Ahmad tea is a power", + mainAction: "Back to outline", + image: CoreAssets.goodWork.swiftUIImage, + onCloseTapped: {}, + okTapped: {}, + nextSectionTapped: {} + ) .previewLayout(.sizeThatFits) .background(Color.gray) } } +//swiftlint:enable all diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift index dfb0b6925..cc5e83022 100644 --- a/Core/Core/View/Base/CourseCellView.swift +++ b/Core/Core/View/Base/CourseCellView.swift @@ -144,3 +144,4 @@ struct CourseCellView_Previews: PreviewProvider { } } +// swiftlint:enable all diff --git a/Core/Core/View/Base/TextWithUrls.swift b/Core/Core/View/Base/TextWithUrls.swift index e5e50d19e..e516a22f8 100644 --- a/Core/Core/View/Base/TextWithUrls.swift +++ b/Core/Core/View/Base/TextWithUrls.swift @@ -70,6 +70,7 @@ public struct TextWithUrls: View { return text } } +// swiftlint:enable shorthand_operator struct TextWithUrls_Previews: PreviewProvider { static var previews: some View { diff --git a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift index 598018893..d16f4d8e5 100644 --- a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift @@ -137,6 +137,7 @@ public struct HandoutsUpdatesDetailView: View { } #if DEBUG +// swiftlint:disable all struct HandoutsUpdatesDetailView_Previews: PreviewProvider { static var previews: some View { @@ -167,4 +168,5 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i ) } } +// swiftlint:enable all #endif diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 56338ea7b..a058dd9e6 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -332,4 +332,5 @@ struct CourseUnitView_Previews: PreviewProvider { ), sectionName: "") } } +//swiftlint:enable all #endif diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift index 23578ac6c..281aee8e8 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift @@ -97,7 +97,10 @@ public struct DiscussionSearchTopicsView: View { .padding(24) .onAppear { Task.detached(priority: .high) { - await viewModel.searchCourses(index: index, searchTerm: viewModel.searchText) + await viewModel.searchCourses( + index: index, + searchTerm: viewModel.searchText + ) } } if viewModel.searchResults.last != post { diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift index 87e7d7116..d6cc6f64c 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift @@ -181,6 +181,7 @@ struct DiscussionView_Previews: PreviewProvider { .loadFonts() } } +// swiftlint:enable all #endif public struct TopicCell: View { diff --git a/Discussion/Discussion/Presentation/Posts/PostsView.swift b/Discussion/Discussion/Presentation/Posts/PostsView.swift index dcfe705bb..e3936c3b3 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsView.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsView.swift @@ -230,7 +230,6 @@ public struct PostsView: View { } #if DEBUG -// swiftlint:disable all struct PostsView_Previews: PreviewProvider { static var previews: some View { let topics = Topics(coursewareTopics: [], nonCoursewareTopics: []) diff --git a/NewEdX/DI/AppAssembly.swift b/NewEdX/DI/AppAssembly.swift index da342ccf2..16cb8a055 100644 --- a/NewEdX/DI/AppAssembly.swift +++ b/NewEdX/DI/AppAssembly.swift @@ -100,3 +100,4 @@ class AppAssembly: Assembly { }.inObjectScope(.container) } } +// swiftlint:enable function_body_length diff --git a/NewEdX/DI/ScreenAssembly.swift b/NewEdX/DI/ScreenAssembly.swift index c1f5514f1..e9fbf6b00 100644 --- a/NewEdX/DI/ScreenAssembly.swift +++ b/NewEdX/DI/ScreenAssembly.swift @@ -15,7 +15,7 @@ import Profile import Course import Discussion -// swiftlint:disable function_body_length +// swiftlint:disable function_body_length type_body_length class ScreenAssembly: Assembly { func assemble(container: Container) { @@ -363,3 +363,4 @@ class ScreenAssembly: Assembly { } } } +// swiftlint:enable function_body_length type_body_length diff --git a/NewEdX/StringExtension.swift b/NewEdX/StringExtension.swift deleted file mode 100644 index 96f2ba425..000000000 --- a/NewEdX/StringExtension.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// StringExtension.swift -// Core -// -// Created by  Stepanok Ivan on 29.09.2022. -// - -import Foundation -import SwiftUI -import Core -import Swinject - -public extension String { - public func injectCSS(colorScheme: ColorScheme) -> String { - let baseUrl = Container.shared.resolve(Config.self)!.baseURL.absoluteString - var replacedHTML = self.replacingOccurrences(of: "../..", with: baseUrl) - - func currentColor() -> String { - switch colorScheme { - case .light: - return "black" - case .dark: - return "white" - @unknown default: - return "black" - } - } - - let style = """ - - - -
- """ - print(">>>> STYLE", style) - return style + replacedHTML - } -} diff --git a/Profile/Profile/Data/ProfileRepository.swift b/Profile/Profile/Data/ProfileRepository.swift index 3e7418c10..0d3d52798 100644 --- a/Profile/Profile/Data/ProfileRepository.swift +++ b/Profile/Profile/Data/ProfileRepository.swift @@ -143,6 +143,7 @@ public class ProfileRepository: ProfileRepositoryProtocol { // Mark - For testing and SwiftUI preview #if DEBUG +// swiftlint:disable all class ProfileRepositoryMock: ProfileRepositoryProtocol { func getMyProfileOffline() throws -> Core.UserProfile { return UserProfile( @@ -212,4 +213,5 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol { } public func saveSettings(_ settings: UserSettings) {} } +// swiftlint:enable all #endif From be5408f410c59402918219ff43f283fe7b5b9968 Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Fri, 30 Jun 2023 14:03:33 +0300 Subject: [PATCH 24/26] Added Fastlane for linting and tests (#44) --- Gemfile | 3 + Gemfile.lock | 218 +++++++++++++++++++++++++++++++++++++++++++++ fastlane/Appfile | 6 ++ fastlane/Fastfile | 33 +++++++ fastlane/README.md | 38 ++++++++ 5 files changed, 298 insertions(+) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 fastlane/Appfile create mode 100644 fastlane/Fastfile create mode 100644 fastlane/README.md diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..7a118b49b --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..0ee64adc5 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,218 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.6) + rexml + addressable (2.8.4) + public_suffix (>= 2.0.2, < 6.0) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.2.0) + aws-partitions (1.782.0) + aws-sdk-core (3.176.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.68.0) + aws-sdk-core (~> 3, >= 3.176.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.127.0) + aws-sdk-core (~> 3, >= 3.176.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.6) + aws-sigv4 (1.6.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.4) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.100.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.2.7) + fastlane (2.213.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.44.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.19.0) + google-apis-core (>= 0.9.0, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.44.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.19.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.6.0) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.6.3) + jwt (2.7.1) + memoist (0.16.2) + mini_magick (4.12.0) + mini_mime (1.1.2) + multi_json (1.15.0) + multipart-post (2.3.0) + nanaimo (0.3.0) + naturally (2.2.1) + optparse (0.1.1) + os (1.1.4) + plist (3.7.0) + public_suffix (5.0.1) + rake (13.0.6) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.5) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + signet (0.17.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.8.2) + unicode-display_width (1.8.0) + webrick (1.8.1) + word_wrap (1.0.0) + xcodeproj (1.22.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-22 + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.4.10 diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 000000000..4282947e2 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,6 @@ +# app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app +# apple_id("[[APPLE_ID]]") # Your Apple Developer Portal username + + +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 000000000..ce734301b --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,33 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +lane :linting do + swiftlint( + mode: :lint, + executable: "Pods/SwiftLint/swiftlint", + # output_file: "swiftlint.result.json", + config_file: ".swiftlint.yml", + raise_if_swiftlint_error: true, + ignore_exit_status: false + ) +end + +lane :unit_tests do + run_tests( + workspace: "NewEdX.xcworkspace", + device: "iPhone 14", + scheme: "NewEdXDev" + ) +end diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 000000000..3f9a38ebc --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,38 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +### linting + +```sh +[bundle exec] fastlane linting +``` + + + +### unit_tests + +```sh +[bundle exec] fastlane unit_tests +``` + + + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). From 8115b6bc7ee83be1a2c27711eb1f50cfeb300af5 Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Wed, 5 Jul 2023 11:01:03 +0300 Subject: [PATCH 25/26] Add firebase to the project (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Firebase analytics and Сrashlytics with a few environments support * fix router issue on iOS 14 * fix code style --- .swiftlint.yml | 2 +- .../Authorization.xcodeproj/project.pbxproj | 4 + .../Presentation/AuthorizationAnalytics.swift | 38 ++ .../Presentation/Login/SignInView.swift | 3 + .../Presentation/Login/SignInViewModel.swift | 11 +- .../Registration/SignUpView.swift | 2 + .../Registration/SignUpViewModel.swift | 51 +- .../Reset Password/ResetPasswordView.swift | 1 + .../ResetPasswordViewModel.swift | 9 +- .../AuthorizationMock.generated.swift | 287 ++++++++++- .../Login/ResetPasswordViewModelTests.swift | 38 +- .../Login/SignInViewModelTests.swift | 44 +- .../Register/SignUpViewModelTests.swift | 22 +- Core/Core/Analytics/AnalyticsManager.swift | 294 +++++++++++ Core/Core/Domain/AuthInteractor.swift | 6 +- Core/Core/Domain/Model/CourseBlockModel.swift | 5 +- Core/Core/Extensions/ViewExtension.swift | 43 +- Core/Core/View/Base/WebUnitView.swift | 4 +- Course/Course.xcodeproj/project.pbxproj | 4 + Course/Course/Data/CourseRepository.swift | 6 +- Course/Course/Domain/CourseInteractor.swift | 1 + .../Container/CourseContainerView.swift | 122 ++--- .../Container/CourseContainerViewModel.swift | 20 + .../Course/Presentation/CourseAnalytics.swift | 47 ++ Course/Course/Presentation/CourseRouter.swift | 8 + .../Details/CourseDetailsView.swift | 3 + .../Details/CourseDetailsViewModel.swift | 9 + .../Outline/ContinueWithView.swift | 22 +- .../Outline/CourseOutlineView.swift | 11 +- .../Outline/CourseVerticalView.swift | 23 +- .../Outline/CourseVerticalViewModel.swift | 3 + .../Unit/CourseNavigationView.swift | 20 + .../Presentation/Unit/CourseUnitView.swift | 3 +- .../Unit/CourseUnitViewModel.swift | 16 + .../Presentation/Video/SubtittlesView.swift | 4 +- Course/CourseTests/CourseMock.generated.swift | 471 +++++++++++++++++- .../CourseContainerViewModelTests.swift | 52 +- .../Details/CourseDetailsViewModelTests.swift | 21 + .../Unit/CourseUnitViewModelTests.swift | 18 + Dashboard/Dashboard.xcodeproj/project.pbxproj | 4 + .../Presentation/DashboardAnalytics.swift | 19 + .../Presentation/DashboardView.swift | 4 +- .../Presentation/DashboardViewModel.swift | 13 +- .../DashboardMock.generated.swift | 191 ++++++- .../DashboardViewModelTests.swift | 12 +- Discovery/Discovery.xcodeproj/project.pbxproj | 4 + .../Presentation/DiscoveryAnalytics.swift | 23 + .../Presentation/DiscoveryView.swift | 6 +- .../Presentation/DiscoveryViewModel.swift | 15 +- .../Discovery/Presentation/SearchView.swift | 11 +- .../Presentation/SearchViewModel.swift | 11 +- .../DiscoveryMock.generated.swift | 226 ++++++++- .../DiscoveryViewModelTests.swift | 15 +- .../Presentation/SearchViewModelTests.swift | 9 + .../Discussion.xcodeproj/project.pbxproj | 4 + .../Presentation/DiscussionAnalytics.swift | 23 + .../DiscussionSearchTopicsView.swift | 10 +- .../DiscussionTopicsView.swift | 2 + .../DiscussionTopicsViewModel.swift | 39 +- .../DiscussionMock.generated.swift | 232 ++++++++- .../DiscussionTopicsViewModelTests.swift | 23 +- NewEdX.xcodeproj/project.pbxproj | 34 +- NewEdX/AnalyticsManager.swift | 369 ++++++++++++++ NewEdX/AppDelegate.swift | 12 + NewEdX/DI/AppAssembly.swift | 32 ++ NewEdX/DI/ScreenAssembly.swift | 26 +- NewEdX/Environment.swift | 37 ++ NewEdX/Info.plist | 4 + NewEdX/MainScreenAnalytics.swift | 16 + NewEdX/RouteController.swift | 7 +- NewEdX/Router.swift | 10 +- NewEdX/View/MainScreenView.swift | 35 +- Podfile | 6 +- Podfile.lock | 140 +++++- Profile/Profile.xcodeproj/project.pbxproj | 4 + .../EditProfile/EditProfileView.swift | 5 +- .../EditProfile/EditProfileViewModel.swift | 7 +- .../Presentation/Profile/ProfileView.swift | 45 +- .../Profile/ProfileViewModel.swift | 3 + .../Presentation/ProfileAnalytics.swift | 33 ++ .../EditProfileViewModelTests.swift | 84 +++- .../Profile/ProfileViewModelTests.swift | 16 + .../ProfileTests/ProfileMock.generated.swift | 296 ++++++++++- 83 files changed, 3580 insertions(+), 285 deletions(-) create mode 100644 Authorization/Authorization/Presentation/AuthorizationAnalytics.swift create mode 100644 Core/Core/Analytics/AnalyticsManager.swift create mode 100644 Course/Course/Presentation/CourseAnalytics.swift create mode 100644 Dashboard/Dashboard/Presentation/DashboardAnalytics.swift create mode 100644 Discovery/Discovery/Presentation/DiscoveryAnalytics.swift create mode 100644 Discussion/Discussion/Presentation/DiscussionAnalytics.swift create mode 100644 NewEdX/AnalyticsManager.swift create mode 100644 NewEdX/MainScreenAnalytics.swift create mode 100644 Profile/Profile/Presentation/ProfileAnalytics.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index a92444936..5cf8633aa 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -51,7 +51,7 @@ function_parameter_count: error: 12 type_name: - min_length: 4 # only warning + min_length: 3 # only warning max_length: # warning and error warning: 40 error: 50 diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index 1ca667fa4..6eed8e006 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 022D04962976DA6500E0059B /* AuthorizationMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022D04952976DA6500E0059B /* AuthorizationMock.generated.swift */; }; 025F40E029D1E2FC0064C183 /* ResetPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025F40DF29D1E2FC0064C183 /* ResetPasswordView.swift */; }; 025F40E229D360E20064C183 /* ResetPasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025F40E129D360E20064C183 /* ResetPasswordViewModel.swift */; }; + 02A2ACDB2A4B016100FBBBBB /* AuthorizationAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A2ACDA2A4B016100FBBBBB /* AuthorizationAnalytics.swift */; }; 02E0618429DC2373006E9024 /* ResetPasswordViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E0618329DC2373006E9024 /* ResetPasswordViewModelTests.swift */; }; 02F3BFE5292533720051930C /* AuthorizationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFE4292533720051930C /* AuthorizationRouter.swift */; }; 071009C728D1DA4F00344290 /* SignInViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 071009C628D1DA4F00344290 /* SignInViewModel.swift */; }; @@ -46,6 +47,7 @@ 022D04952976DA6500E0059B /* AuthorizationMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationMock.generated.swift; sourceTree = ""; }; 025F40DF29D1E2FC0064C183 /* ResetPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPasswordView.swift; sourceTree = ""; }; 025F40E129D360E20064C183 /* ResetPasswordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPasswordViewModel.swift; sourceTree = ""; }; + 02A2ACDA2A4B016100FBBBBB /* AuthorizationAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationAnalytics.swift; sourceTree = ""; }; 02E0618329DC2373006E9024 /* ResetPasswordViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPasswordViewModelTests.swift; sourceTree = ""; }; 02ED50CC29A64B90008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02F3BFE4292533720051930C /* AuthorizationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationRouter.swift; sourceTree = ""; }; @@ -142,6 +144,7 @@ 07169462296D93E000E3DED6 /* Registration */, 025F40DE29D1C1350064C183 /* Reset Password */, 02F3BFE4292533720051930C /* AuthorizationRouter.swift */, + 02A2ACDA2A4B016100FBBBBB /* AuthorizationAnalytics.swift */, ); path = Presentation; sourceTree = ""; @@ -468,6 +471,7 @@ 0770DE7128D0C0E7006D8A5D /* Strings.swift in Sources */, 025F40E229D360E20064C183 /* ResetPasswordViewModel.swift in Sources */, 02066B462906D72F00F4307E /* SignUpViewModel.swift in Sources */, + 02A2ACDB2A4B016100FBBBBB /* AuthorizationAnalytics.swift in Sources */, 025F40E029D1E2FC0064C183 /* ResetPasswordView.swift in Sources */, 020C31CB290BF49900D6DEA2 /* FieldsView.swift in Sources */, 0770DE4E28D0A677006D8A5D /* SignInView.swift in Sources */, diff --git a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift new file mode 100644 index 000000000..6c7a60393 --- /dev/null +++ b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift @@ -0,0 +1,38 @@ +// +// AuthorizationAnalytics.swift +// Authorization +// +// Created by  Stepanok Ivan on 27.06.2023. +// + +import Foundation + +public enum LoginMethod: String { + case password = "Password" + case facebook = "Facebook" + case google = "Google" + case microsoft = "Microsoft" +} + +//sourcery: AutoMockable +public protocol AuthorizationAnalytics { + func setUserID(_ id: String) + func userLogin(method: LoginMethod) + func signUpClicked() + func createAccountClicked() + func registrationSuccess() + func forgotPasswordClicked() + func resetPasswordClicked(success: Bool) +} + +#if DEBUG +class AuthorizationAnalyticsMock: AuthorizationAnalytics { + public func setUserID(_ id: String) {} + public func userLogin(method: LoginMethod) {} + public func signUpClicked() {} + public func createAccountClicked() {} + public func registrationSuccess() {} + public func forgotPasswordClicked() {} + public func resetPasswordClicked(success: Bool) {} +} +#endif diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index 8fe79e5d3..98fc3305a 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -83,12 +83,14 @@ public struct SignInView: View { HStack { Button(AuthLocalization.SignIn.registerBtn) { + viewModel.analytics.signUpClicked() viewModel.router.showRegisterScreen() }.foregroundColor(CoreAssets.accentColor.swiftUIColor) Spacer() Button(AuthLocalization.SignIn.forgotPassBtn) { + viewModel.analytics.forgotPasswordClicked() viewModel.router.showForgotPasswordScreen() }.foregroundColor(CoreAssets.accentColor.swiftUIColor) } @@ -157,6 +159,7 @@ struct SignInView_Previews: PreviewProvider { let vm = SignInViewModel( interactor: AuthInteractor.mock, router: AuthorizationRouterMock(), + analytics: AuthorizationAnalyticsMock(), validator: Validator() ) diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index 6b25ee379..91a136958 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -32,11 +32,16 @@ public class SignInViewModel: ObservableObject { private let interactor: AuthInteractorProtocol let router: AuthorizationRouter + let analytics: AuthorizationAnalytics private let validator: Validator - public init(interactor: AuthInteractorProtocol, router: AuthorizationRouter, validator: Validator) { + public init(interactor: AuthInteractorProtocol, + router: AuthorizationRouter, + analytics: AuthorizationAnalytics, + validator: Validator) { self.interactor = interactor self.router = router + self.analytics = analytics self.validator = validator } @@ -53,7 +58,9 @@ public class SignInViewModel: ObservableObject { isShowProgress = true do { - try await interactor.login(username: username, password: password) + let user = try await interactor.login(username: username, password: password) + analytics.setUserID("\(user.id)") + analytics.userLogin(method: .password) router.showMainScreen() } catch let error { isShowProgress = false diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 7286086f0..6cace79ba 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -96,6 +96,7 @@ public struct SignUpView: View { } else { StyledButton(AuthLocalization.SignUp.createAccountBtn) { Task { + viewModel.analytics.createAccountClicked() await viewModel.registerUser() } } @@ -146,6 +147,7 @@ struct SignUpView_Previews: PreviewProvider { let vm = SignUpViewModel( interactor: AuthInteractor.mock, router: AuthorizationRouterMock(), + analytics: AuthorizationAnalyticsMock(), config: ConfigMock(), cssInjector: CSSInjectorMock(), validator: Validator() diff --git a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift index e983ecd44..c929c80c3 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift @@ -25,6 +25,7 @@ public class SignUpViewModel: ObservableObject { @Published var fields: [FieldConfiguration] = [] let router: AuthorizationRouter + let analytics: AuthorizationAnalytics let config: Config let cssInjector: CSSInjector @@ -34,12 +35,14 @@ public class SignUpViewModel: ObservableObject { public init( interactor: AuthInteractorProtocol, router: AuthorizationRouter, + analytics: AuthorizationAnalytics, config: Config, cssInjector: CSSInjector, validator: Validator ) { self.interactor = interactor self.router = router + self.analytics = analytics self.config = config self.cssInjector = cssInjector self.validator = validator @@ -76,29 +79,31 @@ public class SignUpViewModel: ObservableObject { @MainActor func registerUser() async { - do { - var validateFields: [String: String] = [:] - fields.forEach({ - validateFields[$0.field.name] = $0.text - }) - validateFields["honor_code"] = "true" - validateFields["terms_of_service"] = "true" - let errors = try await interactor.validateRegistrationFields(fields: validateFields) - guard !showErrors(errors: errors) else { return } - isShowProgress = true - try await interactor.registerUser(fields: validateFields) - isShowProgress = false - router.showMainScreen() - - } catch let error { - isShowProgress = false - if case APIError.invalidGrant = error { - errorMessage = CoreLocalization.Error.invalidCredentials - } else if error.isInternetError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError - } + do { + var validateFields: [String: String] = [:] + fields.forEach({ + validateFields[$0.field.name] = $0.text + }) + validateFields["honor_code"] = "true" + validateFields["terms_of_service"] = "true" + let errors = try await interactor.validateRegistrationFields(fields: validateFields) + guard !showErrors(errors: errors) else { return } + isShowProgress = true + let user = try await interactor.registerUser(fields: validateFields) + analytics.setUserID("\(user.id)") + analytics.registrationSuccess() + isShowProgress = false + router.showMainScreen() + + } catch let error { + isShowProgress = false + if case APIError.invalidGrant = error { + errorMessage = CoreLocalization.Error.invalidCredentials + } else if error.isInternetError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError } + } } } diff --git a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift index 6203c1cc5..17d81921d 100644 --- a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift +++ b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift @@ -159,6 +159,7 @@ struct ResetPasswordView_Previews: PreviewProvider { let vm = ResetPasswordViewModel( interactor: AuthInteractor.mock, router: AuthorizationRouterMock(), + analytics: AuthorizationAnalyticsMock(), validator: Validator() ) ResetPasswordView(viewModel: vm) diff --git a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift index c0d275aa1..cf2b1d71a 100644 --- a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift +++ b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift @@ -30,11 +30,16 @@ public class ResetPasswordViewModel: ObservableObject { private let interactor: AuthInteractorProtocol let router: AuthorizationRouter + let analytics: AuthorizationAnalytics private let validator: Validator - public init(interactor: AuthInteractorProtocol, router: AuthorizationRouter, validator: Validator) { + public init(interactor: AuthInteractorProtocol, + router: AuthorizationRouter, + analytics: AuthorizationAnalytics, + validator: Validator) { self.interactor = interactor self.router = router + self.analytics = analytics self.validator = validator } @@ -48,9 +53,11 @@ public class ResetPasswordViewModel: ObservableObject { do { _ = try await interactor.resetPassword(email: email).responseText.hideHtmlTagsAndUrls() isRecovered.wrappedValue.toggle() + analytics.resetPasswordClicked(success: true) isShowProgress = false } catch { isShowProgress = false + analytics.resetPasswordClicked(success: false) if let validationError = error.validationError, let value = validationError.data?["value"] as? String { errorMessage = value diff --git a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift index c709e0088..ddd1dbb74 100644 --- a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift +++ b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift @@ -121,17 +121,20 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws { + open func registerUser(fields: [String: String]) throws -> User { addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void perform?(`fields`) + var __value: User do { - _ = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() as Void + __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() } catch MockError.notStubed { - // do nothing + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") } catch { throw error } + return __value } open func validateRegistrationFields(fields: [String: String]) throws -> [String: String] { @@ -233,6 +236,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -281,10 +287,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given } @@ -410,6 +416,277 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { } } +// MARK: - AuthorizationAnalytics + +open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func setUserID(_ id: String) { + addInvocation(.m_setUserID__id(Parameter.value(`id`))) + let perform = methodPerformValue(.m_setUserID__id(Parameter.value(`id`))) as? (String) -> Void + perform?(`id`) + } + + open func userLogin(method: LoginMethod) { + addInvocation(.m_userLogin__method_method(Parameter.value(`method`))) + let perform = methodPerformValue(.m_userLogin__method_method(Parameter.value(`method`))) as? (LoginMethod) -> Void + perform?(`method`) + } + + open func signUpClicked() { + addInvocation(.m_signUpClicked) + let perform = methodPerformValue(.m_signUpClicked) as? () -> Void + perform?() + } + + open func createAccountClicked() { + addInvocation(.m_createAccountClicked) + let perform = methodPerformValue(.m_createAccountClicked) as? () -> Void + perform?() + } + + open func registrationSuccess() { + addInvocation(.m_registrationSuccess) + let perform = methodPerformValue(.m_registrationSuccess) as? () -> Void + perform?() + } + + open func forgotPasswordClicked() { + addInvocation(.m_forgotPasswordClicked) + let perform = methodPerformValue(.m_forgotPasswordClicked) as? () -> Void + perform?() + } + + open func resetPasswordClicked(success: Bool) { + addInvocation(.m_resetPasswordClicked__success_success(Parameter.value(`success`))) + let perform = methodPerformValue(.m_resetPasswordClicked__success_success(Parameter.value(`success`))) as? (Bool) -> Void + perform?(`success`) + } + + + fileprivate enum MethodType { + case m_setUserID__id(Parameter) + case m_userLogin__method_method(Parameter) + case m_signUpClicked + case m_createAccountClicked + case m_registrationSuccess + case m_forgotPasswordClicked + case m_resetPasswordClicked__success_success(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_setUserID__id(let lhsId), .m_setUserID__id(let rhsId)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "_ id")) + return Matcher.ComparisonResult(results) + + case (.m_userLogin__method_method(let lhsMethod), .m_userLogin__method_method(let rhsMethod)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsMethod, rhs: rhsMethod, with: matcher), lhsMethod, rhsMethod, "method")) + return Matcher.ComparisonResult(results) + + case (.m_signUpClicked, .m_signUpClicked): return .match + + case (.m_createAccountClicked, .m_createAccountClicked): return .match + + case (.m_registrationSuccess, .m_registrationSuccess): return .match + + case (.m_forgotPasswordClicked, .m_forgotPasswordClicked): return .match + + case (.m_resetPasswordClicked__success_success(let lhsSuccess), .m_resetPasswordClicked__success_success(let rhsSuccess)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSuccess, rhs: rhsSuccess, with: matcher), lhsSuccess, rhsSuccess, "success")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_setUserID__id(p0): return p0.intValue + case let .m_userLogin__method_method(p0): return p0.intValue + case .m_signUpClicked: return 0 + case .m_createAccountClicked: return 0 + case .m_registrationSuccess: return 0 + case .m_forgotPasswordClicked: return 0 + case let .m_resetPasswordClicked__success_success(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_setUserID__id: return ".setUserID(_:)" + case .m_userLogin__method_method: return ".userLogin(method:)" + case .m_signUpClicked: return ".signUpClicked()" + case .m_createAccountClicked: return ".createAccountClicked()" + case .m_registrationSuccess: return ".registrationSuccess()" + case .m_forgotPasswordClicked: return ".forgotPasswordClicked()" + case .m_resetPasswordClicked__success_success: return ".resetPasswordClicked(success:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func setUserID(_ id: Parameter) -> Verify { return Verify(method: .m_setUserID__id(`id`))} + public static func userLogin(method: Parameter) -> Verify { return Verify(method: .m_userLogin__method_method(`method`))} + public static func signUpClicked() -> Verify { return Verify(method: .m_signUpClicked)} + public static func createAccountClicked() -> Verify { return Verify(method: .m_createAccountClicked)} + public static func registrationSuccess() -> Verify { return Verify(method: .m_registrationSuccess)} + public static func forgotPasswordClicked() -> Verify { return Verify(method: .m_forgotPasswordClicked)} + public static func resetPasswordClicked(success: Parameter) -> Verify { return Verify(method: .m_resetPasswordClicked__success_success(`success`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func setUserID(_ id: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_setUserID__id(`id`), performs: perform) + } + public static func userLogin(method: Parameter, perform: @escaping (LoginMethod) -> Void) -> Perform { + return Perform(method: .m_userLogin__method_method(`method`), performs: perform) + } + public static func signUpClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_signUpClicked, performs: perform) + } + public static func createAccountClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_createAccountClicked, performs: perform) + } + public static func registrationSuccess(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_registrationSuccess, performs: perform) + } + public static func forgotPasswordClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_forgotPasswordClicked, performs: perform) + } + public static func resetPasswordClicked(success: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_resetPasswordClicked__success_success(`success`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + // MARK: - AuthorizationRouter open class AuthorizationRouterMock: AuthorizationRouter, Mock { diff --git a/Authorization/AuthorizationTests/Presentation/Login/ResetPasswordViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Login/ResetPasswordViewModelTests.swift index 75be4e24f..6b09633f2 100644 --- a/Authorization/AuthorizationTests/Presentation/Login/ResetPasswordViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Login/ResetPasswordViewModelTests.swift @@ -18,7 +18,11 @@ final class ResetPasswordViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = ResetPasswordViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = ResetPasswordViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) var isRecoveryPassword = true let binding = Binding(get: { @@ -40,7 +44,11 @@ final class ResetPasswordViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = ResetPasswordViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = ResetPasswordViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) var isRecoveryPassword = true let binding = Binding(get: { @@ -66,7 +74,11 @@ final class ResetPasswordViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = ResetPasswordViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = ResetPasswordViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) let validationErrorMessage = "Some error" let validationError = CustomValidationError(statusCode: 400, data: ["value": validationErrorMessage]) @@ -94,7 +106,11 @@ final class ResetPasswordViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = ResetPasswordViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = ResetPasswordViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) Given(interactor, .resetPassword(email: .any, willThrow: APIError.invalidGrant)) @@ -118,7 +134,11 @@ final class ResetPasswordViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = ResetPasswordViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = ResetPasswordViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) Given(interactor, .resetPassword(email: .any, willThrow: NSError())) @@ -142,8 +162,12 @@ final class ResetPasswordViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = ResetPasswordViewModel(interactor: interactor, router: router, validator: validator) - + let analytics = AuthorizationAnalyticsMock() + let viewModel = ResetPasswordViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) + let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) Given(interactor, .resetPassword(email: .any, willThrow: noInternetError)) diff --git a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift index 01748fb87..2fa56145d 100644 --- a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift @@ -26,7 +26,11 @@ final class SignInViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = SignInViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) await viewModel.login(username: "email", password: "") @@ -41,8 +45,11 @@ final class SignInViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = SignInViewModel(interactor: interactor, router: router, validator: validator) - + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) await viewModel.login(username: "edxUser@edx.com", password: "") Verify(interactor, 0, .login(username: .any, password: .any)) @@ -56,7 +63,11 @@ final class SignInViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = SignInViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) let user = User(id: 1, username: "username", email: "edxUser@edx.com", name: "Name", userAvatar: "") Given(interactor, .login(username: .any, password: .any, willReturn: user)) @@ -64,6 +75,7 @@ final class SignInViewModelTests: XCTestCase { await viewModel.login(username: "edxUser@edx.com", password: "password123") Verify(interactor, 1, .login(username: .any, password: .any)) + Verify(analytics, .userLogin(method: .any)) Verify(router, 1, .showMainScreen()) XCTAssertEqual(viewModel.errorMessage, nil) @@ -74,7 +86,11 @@ final class SignInViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = SignInViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) let validationErrorMessage = "Some error" let validationError = CustomValidationError(statusCode: 400, data: ["error_description": validationErrorMessage]) @@ -95,7 +111,11 @@ final class SignInViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = SignInViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) Given(interactor, .login(username: .any, password: .any, willThrow: APIError.invalidGrant)) @@ -112,7 +132,11 @@ final class SignInViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = SignInViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) Given(interactor, .login(username: .any, password: .any, willThrow: NSError())) @@ -129,7 +153,11 @@ final class SignInViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() - let viewModel = SignInViewModel(interactor: interactor, router: router, validator: validator) + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) diff --git a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift index 853bb224d..ee463f6ef 100644 --- a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift @@ -26,8 +26,10 @@ final class SignUpViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() + let analytics = AuthorizationAnalyticsMock() let viewModel = SignUpViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), validator: validator) @@ -53,8 +55,10 @@ final class SignUpViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() + let analytics = AuthorizationAnalyticsMock() let viewModel = SignUpViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), validator: validator) @@ -75,8 +79,10 @@ final class SignUpViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() + let analytics = AuthorizationAnalyticsMock() let viewModel = SignUpViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), validator: validator) @@ -95,13 +101,19 @@ final class SignUpViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() + let analytics = AuthorizationAnalyticsMock() let viewModel = SignUpViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), validator: validator) - Given(interactor, .registerUser(fields: .any, willProduce: {_ in})) + Given(interactor, .registerUser(fields: .any, willReturn: .init(id: 1, + username: "Name", + email: "mail", + name: "name", + userAvatar: "avatar"))) Given(interactor, .validateRegistrationFields(fields: .any, willReturn: [:])) await viewModel.registerUser() @@ -118,8 +130,10 @@ final class SignUpViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() + let analytics = AuthorizationAnalyticsMock() let viewModel = SignUpViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), validator: validator) @@ -151,8 +165,10 @@ final class SignUpViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() + let analytics = AuthorizationAnalyticsMock() let viewModel = SignUpViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), validator: validator) @@ -175,8 +191,10 @@ final class SignUpViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() + let analytics = AuthorizationAnalyticsMock() let viewModel = SignUpViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), validator: validator) @@ -199,8 +217,10 @@ final class SignUpViewModelTests: XCTestCase { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() + let analytics = AuthorizationAnalyticsMock() let viewModel = SignUpViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), validator: validator) diff --git a/Core/Core/Analytics/AnalyticsManager.swift b/Core/Core/Analytics/AnalyticsManager.swift new file mode 100644 index 000000000..9972ab43a --- /dev/null +++ b/Core/Core/Analytics/AnalyticsManager.swift @@ -0,0 +1,294 @@ +// +// AnalyticsManager.swift +// NewEdX +// +// Created by  Stepanok Ivan on 27.06.2023. +// + +import Foundation +import FirebaseAnalytics +import Authorization +import Discovery +import Dashboard +import Profile +import Course +import Discussion + +public protocol BaseCourseAnalytics { + associatedtype Event: RawRepresentable where Event.RawValue == String + func logEvent(_ event: Event, parameters: [String: Any]?) +} + +public class AnalyticsManager: BaseCourseAnalytics, + AuthorizationAnalytics, + MainScreenAnalytics, + DiscoveryAnalytics, + DashboardAnalytics, + ProfileAnalytics, + CourseAnalytics, + DiscussionAnalytics { + + public enum Event: String { + case userLogin = "User_Login" + case signUpClicked = "Sign_up_Clicked" + case createAccountClicked = "Create_Account_Clicked" + case registrationSuccess = "Registration_Success" + case userLogout = "User_Logout" + case forgotPasswordClicked = "Forgot_password_Clicked" + case resetPasswordClicked = "Reset_password_Clicked" + + case mainDiscoveryTabClicked = "Main_Discovery_tab_Clicked" + case mainDashboardTabClicked = "Main_Dashboard_tab_Clicked" + case mainProgramsTabClicked = "Main_Programs_tab_Clicked" + case mainProfileTabClicked = "Main_Profile_tab_Clicked" + + case discoverySearchBarClicked = "Discovery_Search_Bar_Clicked" + case discoveryCoursesSearch = "Discovery_Courses_Search" + case discoveryCourseClicked = "Discovery_Course_Clicked" + + case dashboardCourseClicked = "Dashboard_Course_Clicked" + + case profileEditClicked = "Profile_Edit_Clicked" + case profileEditDoneClicked = "Profile_Edit_Done_Clicked" + case profileDeleteAccountClicked = "Profile_Delete_Account_Clicked" + case profileVideoSettingsClicked = "Profile_Video_settings_Clicked" + case privacyPolicyClicked = "Privacy_Policy_Clicked" + case cookiePolicyClicked = "Cookie_Policy_Clicked" + case emailSupportClicked = "Email_Support_Clicked" + + case courseEnrollClicked = "Course_Enroll_Clicked" + case courseEnrollSuccess = "Course_Enroll_Success" + case viewCourseClicked = "View_Course_Clicked" + case resumeCourseTapped = "Resume_Course_Tapped" + case sequentialClicked = "Sequential_Clicked" + case verticalClicked = "Vertical_Clicked" + case nextBlockClicked = "Next_Block_Clicked" + case prevBlockClicked = "Prev_Block_Clicked" + case finishVerticalClicked = "Finish_Vertical_Clicked" + case finishVerticalNextSectionClicked = "Finish_Vertical_Next_section_Clicked" + case finishVerticalBackToOutlineClicked = "Finish_Vertical_Back_to_outline_Clicked" + case courseOutlineCourseTabClicked = "Course_Outline_Course_tab_Clicked" + case courseOutlineVideosTabClicked = "Course_Outline_Videos_tab_Clicked" + case courseOutlineDiscussionTabClicked = "Course_Outline_Discussion_tab_Clicked" + case courseOutlineHandoutsTabClicked = "Course_Outline_Handouts_tab_Clicked" + + case discussionAllPostsClicked = "Discussion_All_Posts_Clicked" + case discussionFollowingClicked = "Discussion_Following_Clicked" + case discussionTopicClicked = "Discussion_Topic_Clicked" + } + + public func logEvent(_ event: Event, parameters: [String: Any]?) { + Analytics.setAnalyticsCollectionEnabled(true) + Analytics.logEvent(event.rawValue, parameters: parameters) + } + + public func userLogin(method: LoginMethod) { + logEvent(.userLogin, parameters: ["method": method.rawValue]) + } + + public func signUpClicked() { + logEvent(.signUpClicked, parameters: nil) + } + + public func createAccountClicked(provider: LoginProvider) { + logEvent(.createAccountClicked, + parameters: ["provider": provider.rawValue]) + } + + public func registrationSuccess(provider: LoginProvider) { + logEvent(.registrationSuccess, + parameters: ["provider": provider.rawValue]) + } + + public func forgotPasswordClicked() { + logEvent(.forgotPasswordClicked, parameters: nil) + } + + public func resetPasswordClicked(success: Bool) { + logEvent(.resetPasswordClicked, parameters: ["success": success]) + } + + // MARK: MainScreenAnalytics + + public func mainDiscoveryTabClicked() { + logEvent(.mainDiscoveryTabClicked, parameters: nil) + } + + public func mainDashboardTabClicked() { + logEvent(.mainDashboardTabClicked, parameters: nil) + } + + public func mainProgramsTabClicked() { + logEvent(.mainProgramsTabClicked, parameters: nil) + } + + public func mainProfileTabClicked() { + logEvent(.mainProfileTabClicked, parameters: nil) + } + + // MARK: Discovery + + public func discoverySearchBarClicked() { + logEvent(.discoverySearchBarClicked, parameters: nil) + } + + public func discoveryCoursesSearch(label: String, coursesCount: Int) { + logEvent(.discoveryCoursesSearch, + parameters: ["label": label, + "courses_count": coursesCount]) + } + + public func discoveryCourseClicked(courseID: String, courseName: String) { + logEvent(.discoveryCourseClicked, parameters: ["course_id": courseID, + "course_name": courseName]) + } + + // MARK: Dashboard + + public func dashboardCourseClicked(courseID: String, courseName: String) { + logEvent(.dashboardCourseClicked, parameters: ["course_id": courseID, + "course_name": courseName]) + } + + // MARK: Profile + + public func profileEditClicked() { + logEvent(.profileEditClicked, parameters: nil) + } + + public func profileEditDoneClicked() { + logEvent(.profileEditDoneClicked, parameters: nil) + } + + public func profileDeleteAccountClicked() { + logEvent(.profileDeleteAccountClicked, parameters: nil) + } + + public func profileVideoSettingsClicked() { + logEvent(.profileVideoSettingsClicked, parameters: nil) + } + + public func privacyPolicyClicked() { + logEvent(.privacyPolicyClicked, parameters: nil) + } + + public func cookiePolicyClicked() { + logEvent(.cookiePolicyClicked, parameters: nil) + } + + public func emailSupportClicked() { + logEvent(.emailSupportClicked, parameters: nil) + } + + public func userLogout(force: Bool) { + logEvent(.userLogout, parameters: ["force": force]) + } + + // MARK: Course + + public func courseEnrollClicked(courseId: String, courseName: String) { + logEvent(.courseEnrollClicked, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + public func courseEnrollSuccess(courseId: String, courseName: String) { + logEvent(.courseEnrollSuccess, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + public func viewCourseClicked(courseId: String, courseName: String) { + logEvent(.viewCourseClicked, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + public func resumeCourseTapped(courseId: String, courseName: String, blockId: String) { + logEvent(.resumeCourseTapped, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + public func sequentialClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + logEvent(.sequentialClicked, parameters: ["course_id": courseId, + "course_name": courseName, + "block_id": blockId, + "block_name": blockName]) + } + + public func verticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + logEvent(.verticalClicked, parameters: ["course_id": courseId, + "course_name": courseName, + "block_id": blockId, + "block_name": blockName]) + } + + public func nextBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + logEvent(.nextBlockClicked, parameters: ["course_id": courseId, + "course_name": courseName, + "block_id": blockId, + "block_name": blockName]) + } + + public func prevBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + logEvent(.prevBlockClicked, parameters: ["course_id": courseId, + "course_name": courseName, + "block_id": blockId, + "block_name": blockName]) + } + + public func finishVerticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + logEvent(.finishVerticalClicked, parameters: ["course_id": courseId, + "course_name": courseName, + "block_id": blockId, + "block_name": blockName]) + } + + public func finishVerticalNextSectionClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + logEvent(.finishVerticalNextSectionClicked, parameters: ["course_id": courseId, + "course_name": courseName, + "block_id": blockId, + "block_name": blockName]) + } + + public func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) { + logEvent(.finishVerticalBackToOutlineClicked, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + public func courseOutlineCourseTabClicked(courseId: String, courseName: String) { + logEvent(.courseOutlineCourseTabClicked, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + public func courseOutlineVideosTabClicked(courseId: String, courseName: String) { + logEvent(.courseOutlineVideosTabClicked, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) { + logEvent(.courseOutlineDiscussionTabClicked, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + public func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) { + logEvent(.courseOutlineHandoutsTabClicked, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + // MARK: Discussion + + public func discussionAllPostsClicked(courseId: String, courseName: String) { + logEvent(.discussionAllPostsClicked, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + public func discussionFollowingClicked(courseId: String, courseName: String) { + logEvent(.discussionFollowingClicked, parameters: ["course_id": courseId, + "course_name": courseName]) + } + + public func discussionTopicClicked(courseId: String, courseName: String, topicId: String, topicName: String) { + logEvent(.discussionAllPostsClicked, parameters: ["course_id": courseId, + "course_name": courseName, + "topic_id": topicId, + "topic_name": topicName]) + } +} diff --git a/Core/Core/Domain/AuthInteractor.swift b/Core/Core/Domain/AuthInteractor.swift index 69bac7296..94e202364 100644 --- a/Core/Core/Domain/AuthInteractor.swift +++ b/Core/Core/Domain/AuthInteractor.swift @@ -14,7 +14,7 @@ public protocol AuthInteractorProtocol { func resetPassword(email: String) async throws -> ResetPassword func getCookies(force: Bool) async throws func getRegistrationFields() async throws -> [PickerFields] - func registerUser(fields: [String: String]) async throws + func registerUser(fields: [String: String]) async throws -> User func validateRegistrationFields(fields: [String: String]) async throws -> [String: String] } @@ -43,8 +43,8 @@ public class AuthInteractor: AuthInteractorProtocol { return try await repository.getRegistrationFields() } - public func registerUser(fields: [String: String]) async throws { - _ = try await repository.registerUser(fields: fields) + public func registerUser(fields: [String: String]) async throws -> User { + return try await repository.registerUser(fields: fields) } public func validateRegistrationFields(fields: [String: String]) async throws -> [String: String] { diff --git a/Core/Core/Domain/Model/CourseBlockModel.swift b/Core/Core/Domain/Model/CourseBlockModel.swift index 2187b2c8f..0165d9ffb 100644 --- a/Core/Core/Domain/Model/CourseBlockModel.swift +++ b/Core/Core/Domain/Model/CourseBlockModel.swift @@ -9,6 +9,7 @@ import Foundation public struct CourseStructure: Equatable { + public let courseID: String public let id: String public let graded: Bool public let completion: Double @@ -20,7 +21,8 @@ public struct CourseStructure: Equatable { public let media: DataLayer.CourseMedia public let certificate: Certificate? - public init(id: String, + public init(courseID: String, + id: String, graded: Bool, completion: Double, viewYouTubeUrl: String, @@ -30,6 +32,7 @@ public struct CourseStructure: Equatable { childs: [CourseChapter], media: DataLayer.CourseMedia, certificate: Certificate?) { + self.courseID = courseID self.id = id self.graded = graded self.completion = completion diff --git a/Core/Core/Extensions/ViewExtension.swift b/Core/Core/Extensions/ViewExtension.swift index cab274996..9dc0a8818 100644 --- a/Core/Core/Extensions/ViewExtension.swift +++ b/Core/Core/Extensions/ViewExtension.swift @@ -6,43 +6,10 @@ // import Foundation -import Introspect +import SwiftUIIntrospect import SwiftUI public extension View { - func introspectCollectionView(customize: @escaping (UICollectionView) -> Void) -> some View { - return inject(UIKitIntrospectionView( - selector: { introspectionView in - guard let viewHost = Introspect.findViewHost(from: introspectionView) else { - return nil - } - return Introspect.previousSibling(containing: UICollectionView.self, from: viewHost) - }, - customize: customize - )) - } - - func introspectCollectionViewWithClipping(customize: @escaping (UICollectionView) -> Void) -> some View { - return inject(UIKitIntrospectionView( - selector: { introspectionView in - guard let viewHost = Introspect.findViewHost(from: introspectionView) else { - return nil - } - // first run Introspect as normal - if let selectedView = Introspect.previousSibling(containing: UICollectionView.self, - from: viewHost) { - return selectedView - } else if let superView = viewHost.superview { - // if no view was found and a superview exists, search the superview as well - return Introspect.previousSibling(containing: UICollectionView.self, from: superView) - } else { - // no view found at all - return nil - } - }, - customize: customize - )) - } func cardStyle( top: CGFloat? = 0, @@ -155,8 +122,12 @@ public extension View { if #available(iOS 16.0, *) { return self.navigationBarHidden(true) } else { - return self.introspectNavigationController { $0.isNavigationBarHidden = true } - .navigationBarHidden(true) + return self.introspect( + .navigationView(style: .stack), + on: .iOS(.v14, .v15, .v16, .v17), + scope: .ancestor) { + $0.isNavigationBarHidden = true + } } } diff --git a/Core/Core/View/Base/WebUnitView.swift b/Core/Core/View/Base/WebUnitView.swift index 85285a21b..e1108b3dc 100644 --- a/Core/Core/View/Base/WebUnitView.swift +++ b/Core/Core/View/Base/WebUnitView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Introspect +import SwiftUIIntrospect public struct WebUnitView: View { @@ -58,7 +58,7 @@ public struct WebUnitView: View { isLoading: $isWebViewLoading, refreshCookies: { await viewModel.updateCookies(force: true) }) - .introspectScrollView(customize: { scrollView in + .introspect(.scrollView, on: .iOS(.v14, .v15, .v16, .v17), customize: { scrollView in scrollView.isScrollEnabled = false }) .frame(width: reader.size.width, height: reader.size.height) diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index 6a9324697..5af6ffb85 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 02F0144F28F46474002E513D /* CourseContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F0144E28F46474002E513D /* CourseContainerView.swift */; }; 02F0145728F4A2FF002E513D /* CourseContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F0145628F4A2FF002E513D /* CourseContainerViewModel.swift */; }; 02F066E829DC71750073E13B /* SubtittlesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F066E729DC71750073E13B /* SubtittlesView.swift */; }; + 02F175372A4DAFD20019CD70 /* CourseAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F175362A4DAFD20019CD70 /* CourseAnalytics.swift */; }; 02F3BFDD29252E900051930C /* CourseRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFDC29252E900051930C /* CourseRouter.swift */; }; 02F78AEB29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F78AEA29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift */; }; 02F98A8128F8224200DE94C0 /* Discussion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02F98A8028F8224200DE94C0 /* Discussion.framework */; }; @@ -124,6 +125,7 @@ 02F0144E28F46474002E513D /* CourseContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseContainerView.swift; sourceTree = ""; }; 02F0145628F4A2FF002E513D /* CourseContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseContainerViewModel.swift; sourceTree = ""; }; 02F066E729DC71750073E13B /* SubtittlesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtittlesView.swift; sourceTree = ""; }; + 02F175362A4DAFD20019CD70 /* CourseAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseAnalytics.swift; sourceTree = ""; }; 02F3BFDC29252E900051930C /* CourseRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseRouter.swift; sourceTree = ""; }; 02F78AEA29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerViewModelTests.swift; path = CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift; sourceTree = SOURCE_ROOT; }; 02F98A8028F8224200DE94C0 /* Discussion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discussion.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -324,6 +326,7 @@ 070019A928F6F59D00D5FC78 /* Unit */, 070019AA28F6F79E00D5FC78 /* Video */, 02F3BFDC29252E900051930C /* CourseRouter.swift */, + 02F175362A4DAFD20019CD70 /* CourseAnalytics.swift */, ); path = Presentation; sourceTree = ""; @@ -711,6 +714,7 @@ 0766DFCC299AA7A600EBEF6A /* YouTubeVideoPlayer.swift in Sources */, 022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */, 02E685BE28E4B60A000AE015 /* CourseDetailsView.swift in Sources */, + 02F175372A4DAFD20019CD70 /* CourseAnalytics.swift in Sources */, 02B6B3BE28E1D15C00232911 /* CourseDetailsEndpoint.swift in Sources */, 02B6B3C328E1DCD100232911 /* CourseDetails.swift in Sources */, ); diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index f042b9792..55f97a869 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -122,7 +122,8 @@ public class CourseRepository: CourseRepositoryProtocol { childs.append(chapter) } - return CourseStructure(id: course.id, + return CourseStructure(courseID: structure.id, + id: course.id, graded: course.graded, completion: course.completion ?? 0, viewYouTubeUrl: course.userViewData?.encodedVideo?.youTube?.url ?? "", @@ -318,7 +319,8 @@ And there are various ways of describing it-- call it oral poetry or childs.append(chapter) } - return CourseStructure(id: course.id, + return CourseStructure(courseID: structure.id, + id: course.id, graded: course.graded, completion: course.completion ?? 0, viewYouTubeUrl: course.userViewData?.encodedVideo?.youTube?.url ?? "", diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift index abdbd40d4..3bcfdd574 100644 --- a/Course/Course/Domain/CourseInteractor.swift +++ b/Course/Course/Domain/CourseInteractor.swift @@ -48,6 +48,7 @@ public class CourseInteractor: CourseInteractorProtocol { } } return CourseStructure( + courseID: course.courseID, id: course.id, graded: course.graded, completion: course.completion, diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 9c698850c..69a9755f7 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -18,7 +18,7 @@ public struct CourseContainerView: View { private var courseID: String private var title: String - public enum CourseTab { + enum CourseTab { case course case videos case discussion @@ -39,72 +39,77 @@ public struct CourseContainerView: View { } public var body: some View { - if let courseStart = viewModel.courseStart { - if courseStart > Date() { - CourseOutlineView( - viewModel: viewModel, - title: title, - courseID: courseID, - isVideo: false - ) - } else { - TabView(selection: $selection) { + ZStack { + if let courseStart = viewModel.courseStart { + if courseStart > Date() { CourseOutlineView( - viewModel: self.viewModel, + viewModel: viewModel, title: title, courseID: courseID, isVideo: false ) - .tabItem { - CoreAssets.bookCircle.swiftUIImage.renderingMode(.template) - Text(CourseLocalization.CourseContainer.course) + } else { + TabView(selection: $selection) { + CourseOutlineView( + viewModel: self.viewModel, + title: title, + courseID: courseID, + isVideo: false + ) + .tabItem { + CoreAssets.bookCircle.swiftUIImage.renderingMode(.template) + Text(CourseLocalization.CourseContainer.course) + } + .tag(CourseTab.course) + .hideNavigationBar() + + CourseOutlineView( + viewModel: self.viewModel, + title: title, + courseID: courseID, + isVideo: true + ) + .tabItem { + CoreAssets.videoCircle.swiftUIImage.renderingMode(.template) + Text(CourseLocalization.CourseContainer.videos) + } + .tag(CourseTab.videos) + .hideNavigationBar() + + DiscussionTopicsView(courseID: courseID, + viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self, + argument: title)!, + router: Container.shared.resolve(DiscussionRouter.self)!) + .tabItem { + CoreAssets.bubbleLeftCircle.swiftUIImage.renderingMode(.template) + Text(CourseLocalization.CourseContainer.discussion) + } + .tag(CourseTab.discussion) + .hideNavigationBar() + + HandoutsView(courseID: courseID, + viewModel: Container.shared.resolve(HandoutsViewModel.self, argument: courseID)!) + .tabItem { + CoreAssets.docCircle.swiftUIImage.renderingMode(.template) + Text(CourseLocalization.CourseContainer.handouts) + } + .tag(CourseTab.handounds) + .hideNavigationBar() } - .tag(CourseTab.course) - .hideNavigationBar() - - CourseOutlineView( - viewModel: self.viewModel, - title: title, - courseID: courseID, - isVideo: true - ) - .tabItem { - CoreAssets.videoCircle.swiftUIImage.renderingMode(.template) - Text(CourseLocalization.CourseContainer.videos) - } - .tag(CourseTab.videos) - .hideNavigationBar() - - DiscussionTopicsView(courseID: courseID, - viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self)!, - router: Container.shared.resolve(DiscussionRouter.self)!) - .tabItem { - CoreAssets.bubbleLeftCircle.swiftUIImage.renderingMode(.template) - Text(CourseLocalization.CourseContainer.discussion) - } - .tag(CourseTab.discussion) - .hideNavigationBar() - - HandoutsView(courseID: courseID, - viewModel: Container.shared.resolve(HandoutsViewModel.self, argument: courseID)!) - .tabItem { - CoreAssets.docCircle.swiftUIImage.renderingMode(.template) - Text(CourseLocalization.CourseContainer.handouts) - } - .tag(CourseTab.handounds) - .hideNavigationBar() - } - .navigationBarHidden(true) - .introspectViewController { vc in - vc.navigationController?.setNavigationBarHidden(true, animated: false) - } - .onFirstAppear { - Task { - await viewModel.tryToRefreshCookies() + .onFirstAppear { + Task { + await viewModel.tryToRefreshCookies() + } } } } - } + }.onChange(of: selection, perform: { selection in + viewModel.trackSelectedTab( + selection: selection, + courseId: courseID, + courseName: title + ) + }) } } @@ -116,6 +121,7 @@ struct CourseScreensView_Previews: PreviewProvider { interactor: CourseInteractor.mock, authInteractor: AuthInteractor.mock, router: CourseRouterMock(), + analytics: CourseAnalyticsMock(), config: ConfigMock(), connectivity: Connectivity(), manager: DownloadManagerMock(), diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index c13b68edc..4f948bfa9 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -30,6 +30,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { private let interactor: CourseInteractorProtocol private let authInteractor: AuthInteractorProtocol let router: CourseRouter + let analytics: CourseAnalytics let config: Config let connectivity: ConnectivityProtocol @@ -43,6 +44,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { interactor: CourseInteractorProtocol, authInteractor: AuthInteractorProtocol, router: CourseRouter, + analytics: CourseAnalytics, config: Config, connectivity: ConnectivityProtocol, manager: DownloadManagerProtocol, @@ -55,6 +57,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { self.interactor = interactor self.authInteractor = authInteractor self.router = router + self.analytics = analytics self.config = config self.connectivity = connectivity self.isActive = isActive @@ -149,6 +152,23 @@ public class CourseContainerViewModel: BaseCourseViewModel { } } + func trackSelectedTab( + selection: CourseContainerView.CourseTab, + courseId: String, + courseName: String + ) { + switch selection { + case .course: + analytics.courseOutlineCourseTabClicked(courseId: courseId, courseName: courseName) + case .videos: + analytics.courseOutlineVideosTabClicked(courseId: courseId, courseName: courseName) + case .discussion: + analytics.courseOutlineDiscussionTabClicked(courseId: courseId, courseName: courseName) + case .handounds: + analytics.courseOutlineHandoutsTabClicked(courseId: courseId, courseName: courseName) + } + } + @MainActor private func setDownloadsStates() { guard let courseStructure else { return } diff --git a/Course/Course/Presentation/CourseAnalytics.swift b/Course/Course/Presentation/CourseAnalytics.swift new file mode 100644 index 000000000..914774946 --- /dev/null +++ b/Course/Course/Presentation/CourseAnalytics.swift @@ -0,0 +1,47 @@ +// +// CourseAnalytics.swift +// Course +// +// Created by  Stepanok Ivan on 29.06.2023. +// + +import Foundation + +//sourcery: AutoMockable +public protocol CourseAnalytics { + func courseEnrollClicked(courseId: String, courseName: String) + func courseEnrollSuccess(courseId: String, courseName: String) + func viewCourseClicked(courseId: String, courseName: String) + func resumeCourseTapped(courseId: String, courseName: String, blockId: String) + func sequentialClicked(courseId: String, courseName: String, blockId: String, blockName: String) + func verticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) + func nextBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) + func prevBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) + func finishVerticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) + func finishVerticalNextSectionClicked(courseId: String, courseName: String, blockId: String, blockName: String) + func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) + func courseOutlineCourseTabClicked(courseId: String, courseName: String) + func courseOutlineVideosTabClicked(courseId: String, courseName: String) + func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) + func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) +} + +#if DEBUG +class CourseAnalyticsMock: CourseAnalytics { + public func courseEnrollClicked(courseId: String, courseName: String) {} + public func courseEnrollSuccess(courseId: String, courseName: String) {} + public func viewCourseClicked(courseId: String, courseName: String) {} + public func resumeCourseTapped(courseId: String, courseName: String, blockId: String) {} + public func sequentialClicked(courseId: String, courseName: String, blockId: String, blockName: String) {} + public func verticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) {} + public func nextBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) {} + public func prevBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) {} + public func finishVerticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) {} + public func finishVerticalNextSectionClicked(courseId: String, courseName: String, blockId: String, blockName: String) {} + public func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) {} + public func courseOutlineCourseTabClicked(courseId: String, courseName: String) {} + public func courseOutlineVideosTabClicked(courseId: String, courseName: String) {} + public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) {} + public func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) {} +} +#endif diff --git a/Course/Course/Presentation/CourseRouter.swift b/Course/Course/Presentation/CourseRouter.swift index 58f5944c6..b6e2832f3 100644 --- a/Course/Course/Presentation/CourseRouter.swift +++ b/Course/Course/Presentation/CourseRouter.swift @@ -21,6 +21,7 @@ public protocol CourseRouter: BaseRouter { ) func showCourseUnit( + courseName: String, id: String, blockId: String, courseID: String, @@ -33,6 +34,7 @@ public protocol CourseRouter: BaseRouter { func replaceCourseUnit( id: String, + courseName: String, blockId: String, courseID: String, sectionName: String, @@ -44,6 +46,8 @@ public protocol CourseRouter: BaseRouter { func showCourseVerticalView( id: String, + courseID: String, + courseName: String, title: String, chapters: [CourseChapter], chapterIndex: Int, @@ -75,6 +79,7 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { ) {} public func showCourseUnit( + courseName: String, id: String, blockId: String, courseID: String, @@ -87,6 +92,7 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { public func replaceCourseUnit( id: String, + courseName: String, blockId: String, courseID: String, sectionName: String, @@ -98,6 +104,8 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { public func showCourseVerticalView( id: String, + courseID: String, + courseName: String, title: String, chapters: [CourseChapter], chapterIndex: Int, diff --git a/Course/Course/Presentation/Details/CourseDetailsView.swift b/Course/Course/Presentation/Details/CourseDetailsView.swift index 6d494854a..b7556292e 100644 --- a/Course/Course/Presentation/Details/CourseDetailsView.swift +++ b/Course/Course/Presentation/Details/CourseDetailsView.swift @@ -212,6 +212,8 @@ private struct CourseStateView: View { .padding(.vertical, 24) case .alreadyEnrolled: StyledButton(CourseLocalization.Details.viewCourse, action: { + viewModel.viewCourseClicked(courseId: courseDetails.courseID, + courseName: courseDetails.courseTitle) viewModel.router.showCourseScreens( courseID: courseDetails.courseID, isActive: nil, @@ -324,6 +326,7 @@ struct CourseDetailsView_Previews: PreviewProvider { let vm = CourseDetailsViewModel( interactor: CourseInteractor.mock, router: CourseRouterMock(), + analytics: CourseAnalyticsMock(), config: ConfigMock(), cssInjector: CSSInjectorMock(), connectivity: Connectivity() diff --git a/Course/Course/Presentation/Details/CourseDetailsViewModel.swift b/Course/Course/Presentation/Details/CourseDetailsViewModel.swift index d0b0d4216..6b1e6d747 100644 --- a/Course/Course/Presentation/Details/CourseDetailsViewModel.swift +++ b/Course/Course/Presentation/Details/CourseDetailsViewModel.swift @@ -30,6 +30,7 @@ public class CourseDetailsViewModel: ObservableObject { } private let interactor: CourseInteractorProtocol + private let analytics: CourseAnalytics let router: CourseRouter let config: Config let cssInjector: CSSInjector @@ -38,12 +39,14 @@ public class CourseDetailsViewModel: ObservableObject { public init( interactor: CourseInteractorProtocol, router: CourseRouter, + analytics: CourseAnalytics, config: Config, cssInjector: CSSInjector, connectivity: ConnectivityProtocol ) { self.interactor = interactor self.router = router + self.analytics = analytics self.config = config self.cssInjector = cssInjector self.connectivity = connectivity @@ -101,10 +104,16 @@ public class CourseDetailsViewModel: ObservableObject { UIApplication.shared.open(url) } + func viewCourseClicked(courseId: String, courseName: String) { + analytics.viewCourseClicked(courseId: courseId, courseName: courseName) + } + @MainActor func enrollToCourse(id: String) async { do { + analytics.courseEnrollClicked(courseId: id, courseName: courseDetails?.courseTitle ?? "") _ = try await interactor.enrollToCourse(courseID: id) + analytics.courseEnrollSuccess(courseId: id, courseName: courseDetails?.courseTitle ?? "") courseDetails?.isEnrolled = true NotificationCenter.default.post(name: .onCourseEnrolled, object: id) } catch let error { diff --git a/Course/Course/Presentation/Outline/ContinueWithView.swift b/Course/Course/Presentation/Outline/ContinueWithView.swift index 9498ec471..998ddeca2 100644 --- a/Course/Course/Presentation/Outline/ContinueWithView.swift +++ b/Course/Course/Presentation/Outline/ContinueWithView.swift @@ -18,13 +18,15 @@ struct ContinueWithView: View { let data: ContinueWith let courseStructure: CourseStructure let router: CourseRouter + let analytics: CourseAnalytics private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - init(data: ContinueWith, courseStructure: CourseStructure, router: CourseRouter) { + init(data: ContinueWith, courseStructure: CourseStructure, router: CourseRouter, analytics: CourseAnalytics) { self.data = data self.courseStructure = courseStructure self.router = router + self.analytics = analytics } var body: some View { @@ -38,7 +40,13 @@ struct ContinueWithView: View { }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() UnitButtonView(type: .continueLesson, action: { + analytics.resumeCourseTapped(courseId: courseStructure.courseID, + courseName: courseStructure.displayName, + blockId: chapter.childs[data.sequentialIndex] + .childs[data.verticalIndex].blockId) router.showCourseVerticalView(id: courseStructure.id, + courseID: courseStructure.courseID, + courseName: courseStructure.displayName, title: chapter.childs[data.sequentialIndex].displayName, chapters: courseStructure.childs, chapterIndex: data.chapterIndex, @@ -52,7 +60,13 @@ struct ContinueWithView: View { .foregroundColor(CoreAssets.textPrimary.swiftUIColor) } UnitButtonView(type: .continueLesson, action: { + analytics.resumeCourseTapped(courseId: courseStructure.courseID, + courseName: courseStructure.displayName, + blockId: chapter.childs[data.sequentialIndex] + .childs[data.verticalIndex].blockId) router.showCourseVerticalView(id: courseStructure.id, + courseID: courseStructure.courseID, + courseName: courseStructure.displayName, title: chapter.childs[data.sequentialIndex].displayName, chapters: courseStructure.childs, chapterIndex: data.chapterIndex, @@ -121,7 +135,8 @@ struct ContinueWithView_Previews: PreviewProvider { ] ContinueWithView(data: ContinueWith(chapterIndex: 0, sequentialIndex: 0, verticalIndex: 0), - courseStructure: CourseStructure(id: "123", + courseStructure: CourseStructure(courseID: "v1-course", + id: "123", graded: true, completion: 0, viewYouTubeUrl: "", @@ -131,7 +146,8 @@ struct ContinueWithView_Previews: PreviewProvider { media: DataLayer.CourseMedia.init(image: .init(raw: "", small: "", large: "")), certificate: nil), - router: CourseRouterMock()) + router: CourseRouterMock(), + analytics: CourseAnalyticsMock()) } } #endif diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 4e3b309a6..b0f06ef29 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -95,7 +95,8 @@ public struct CourseOutlineView: View { ContinueWithView( data: continueWith, courseStructure: courseStructure, - router: viewModel.router + router: viewModel.router, + analytics: viewModel.analytics ) } } @@ -119,8 +120,15 @@ public struct CourseOutlineView: View { VStack(alignment: .leading) { Button(action: { if let chapterIndex, let sequentialIndex { + viewModel.analytics + .sequentialClicked(courseId: courseID, + courseName: self.title, + blockId: child.blockId, + blockName: child.displayName) viewModel.router.showCourseVerticalView( id: courseID, + courseID: courseStructure.courseID, + courseName: viewModel.courseStructure?.displayName ?? "", title: child.displayName, chapters: chapters, chapterIndex: chapterIndex, @@ -254,6 +262,7 @@ struct CourseOutlineView_Previews: PreviewProvider { interactor: CourseInteractor.mock, authInteractor: AuthInteractor.mock, router: CourseRouterMock(), + analytics: CourseAnalyticsMock(), config: ConfigMock(), connectivity: Connectivity(), manager: DownloadManagerMock(), diff --git a/Course/Course/Presentation/Outline/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVerticalView.swift index 653f78e76..cf72ea53a 100644 --- a/Course/Course/Presentation/Outline/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVerticalView.swift @@ -13,6 +13,8 @@ import Kingfisher public struct CourseVerticalView: View { private var title: String + private var courseName: String + private var courseID: String private let id: String @ObservedObject private var viewModel: CourseVerticalViewModel @@ -20,10 +22,14 @@ public struct CourseVerticalView: View { public init( title: String, + courseName: String, + courseID: String, id: String, viewModel: CourseVerticalViewModel ) { self.title = title + self.courseName = courseName + self.courseID = courseID self.id = id self.viewModel = viewModel } @@ -42,10 +48,16 @@ public struct CourseVerticalView: View { ForEach(viewModel.verticals, id: \.id) { vertical in if let index = viewModel.verticals.firstIndex(where: {$0.id == vertical.id}) { Button(action: { - if let block = viewModel.verticals[index].childs.first { - viewModel.router.showCourseUnit(id: id, + let vertical = viewModel.verticals[index] + if let block = vertical.childs.first { + viewModel.analytics.verticalClicked(courseId: courseID, + courseName: courseName, + blockId: vertical.blockId, + blockName: vertical.displayName) + viewModel.router.showCourseUnit(courseName: courseName, + id: id, blockId: block.id, - courseID: block.blockId, + courseID: courseID, sectionName: block.displayName, verticalIndex: index, chapters: viewModel.chapters, @@ -190,15 +202,16 @@ struct CourseVerticalView_Previews: PreviewProvider { sequentialIndex: 0, manager: DownloadManagerMock(), router: CourseRouterMock(), + analytics: CourseAnalyticsMock(), connectivity: Connectivity() ) return Group { - CourseVerticalView(title: "Course title", id: "1", viewModel: viewModel) + CourseVerticalView(title: "Course title", courseName: "CourseName", courseID: "1", id: "1", viewModel: viewModel) .preferredColorScheme(.light) .previewDisplayName("CourseVerticalView Light") - CourseVerticalView(title: "Course title", id: "1", viewModel: viewModel) + CourseVerticalView(title: "Course title", courseName: "CourseName", courseID: "1", id: "1", viewModel: viewModel) .preferredColorScheme(.dark) .previewDisplayName("CourseVerticalView Dark") } diff --git a/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift b/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift index 9d0bd77c5..793fb7519 100644 --- a/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift +++ b/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift @@ -11,6 +11,7 @@ import Combine public class CourseVerticalViewModel: BaseCourseViewModel { let router: CourseRouter + let analytics: CourseAnalytics let connectivity: ConnectivityProtocol @Published var verticals: [CourseVertical] @Published var downloadState: [String: DownloadViewState] = [:] @@ -33,12 +34,14 @@ public class CourseVerticalViewModel: BaseCourseViewModel { sequentialIndex: Int, manager: DownloadManagerProtocol, router: CourseRouter, + analytics: CourseAnalytics, connectivity: ConnectivityProtocol ) { self.chapters = chapters self.chapterIndex = chapterIndex self.sequentialIndex = sequentialIndex self.router = router + self.analytics = analytics self.connectivity = connectivity self.verticals = chapters[chapterIndex].childs[sequentialIndex].childs super.init(manager: manager) diff --git a/Course/Course/Presentation/Unit/CourseNavigationView.swift b/Course/Course/Presentation/Unit/CourseNavigationView.swift index a4b9f9d85..97db77c0d 100644 --- a/Course/Course/Presentation/Unit/CourseNavigationView.swift +++ b/Course/Course/Presentation/Unit/CourseNavigationView.swift @@ -72,6 +72,9 @@ struct CourseNavigationView: View { okTapped: { playerStateSubject.send(VideoPlayerState.pause) playerStateSubject.send(VideoPlayerState.kill) + viewModel.analytics + .finishVerticalBackToOutlineClicked(courseId: viewModel.courseID, + courseName: viewModel.courseName) viewModel.router.dismiss(animated: false) viewModel.router.back(animated: true) }, @@ -101,8 +104,17 @@ struct CourseNavigationView: View { verticalIndex = 0 } + viewModel.analytics + .finishVerticalNextSectionClicked( + courseId: viewModel.courseID, + courseName: viewModel.courseName, + blockId: viewModel.selectedLesson().blockId, + blockName: viewModel.selectedLesson().displayName + ) + viewModel.router.replaceCourseUnit( id: viewModel.id, + courseName: viewModel.courseName, blockId: viewModel.lessonID, courseID: viewModel.courseID, sectionName: viewModel.selectedLesson().displayName, @@ -112,6 +124,12 @@ struct CourseNavigationView: View { sequentialIndex: sequentialIndex) } ) + viewModel.analytics.finishVerticalClicked( + courseId: viewModel.courseID, + courseName: viewModel.courseName, + blockId: viewModel.selectedLesson().blockId, + blockName: viewModel.selectedLesson().displayName + ) }) } else { if viewModel.selectedLesson() != viewModel.verticals[viewModel.verticalIndex].childs.first { @@ -139,12 +157,14 @@ struct CourseNavigationView_Previews: PreviewProvider { lessonID: "1", courseID: "1", id: "1", + courseName: "Name", chapters: [], chapterIndex: 1, sequentialIndex: 1, verticalIndex: 1, interactor: CourseInteractor.mock, router: CourseRouterMock(), + analytics: CourseAnalyticsMock(), connectivity: Connectivity(), manager: DownloadManagerMock() ) diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index a058dd9e6..d8b06640e 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -10,7 +10,6 @@ import SwiftUI import Core import Discussion import Swinject -import Introspect import Combine public struct CourseUnitView: View { @@ -321,12 +320,14 @@ struct CourseUnitView_Previews: PreviewProvider { lessonID: "", courseID: "", id: "1", + courseName: "courseName", chapters: chapters, chapterIndex: 0, sequentialIndex: 0, verticalIndex: 0, interactor: CourseInteractor.mock, router: CourseRouterMock(), + analytics: CourseAnalyticsMock(), connectivity: Connectivity(), manager: DownloadManagerMock() ), sectionName: "") diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index 2705f2e3a..cca727927 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -49,6 +49,7 @@ public class CourseUnitViewModel: ObservableObject { var verticals: [CourseVertical] var verticalIndex: Int + var courseName: String @Published var index: Int = 0 var previousLesson: String = "" @@ -66,6 +67,7 @@ public class CourseUnitViewModel: ObservableObject { private let interactor: CourseInteractorProtocol let router: CourseRouter + let analytics: CourseAnalytics let connectivity: ConnectivityProtocol private let manager: DownloadManagerProtocol private var subtitlesDownloaded: Bool = false @@ -81,18 +83,21 @@ public class CourseUnitViewModel: ObservableObject { lessonID: String, courseID: String, id: String, + courseName: String, chapters: [CourseChapter], chapterIndex: Int, sequentialIndex: Int, verticalIndex: Int, interactor: CourseInteractorProtocol, router: CourseRouter, + analytics: CourseAnalytics, connectivity: ConnectivityProtocol, manager: DownloadManagerProtocol ) { self.lessonID = lessonID self.courseID = courseID self.id = id + self.courseName = courseName self.chapters = chapters self.chapterIndex = chapterIndex self.sequentialIndex = sequentialIndex @@ -100,6 +105,7 @@ public class CourseUnitViewModel: ObservableObject { self.verticals = chapters[chapterIndex].childs[sequentialIndex].childs self.interactor = interactor self.router = router + self.analytics = analytics self.connectivity = connectivity self.manager = manager } @@ -119,10 +125,20 @@ public class CourseUnitViewModel: ObservableObject { switch move { case .next: if index != verticals[verticalIndex].childs.count - 1 { index += 1 } + let nextBlock = verticals[verticalIndex].childs[index] nextTitles() + analytics.nextBlockClicked(courseId: courseID, + courseName: courseName, + blockId: nextBlock.blockId, + blockName: nextBlock.displayName) case .previous: if index != 0 { index -= 1 } nextTitles() + let prevBlock = verticals[verticalIndex].childs[index] + analytics.prevBlockClicked(courseId: courseID, + courseName: courseName, + blockId: prevBlock.blockId, + blockName: prevBlock.displayName) } } diff --git a/Course/Course/Presentation/Video/SubtittlesView.swift b/Course/Course/Presentation/Video/SubtittlesView.swift index d09e89967..befc34f68 100644 --- a/Course/Course/Presentation/Video/SubtittlesView.swift +++ b/Course/Course/Presentation/Video/SubtittlesView.swift @@ -75,8 +75,8 @@ public struct SubtittlesView: View { }.id(subtitle.id) } } - .introspectScrollView(customize: { scroll in - scroll.isScrollEnabled = false + .introspect(.scrollView, on: .iOS(.v14, .v15, .v16, .v17), customize: { scrollView in + scrollView.isScrollEnabled = false }) } } diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index e909b96b1..ff0dc4ced 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -121,17 +121,20 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws { + open func registerUser(fields: [String: String]) throws -> User { addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void perform?(`fields`) + var __value: User do { - _ = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() as Void + __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() } catch MockError.notStubed { - // do nothing + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") } catch { throw error } + return __value } open func validateRegistrationFields(fields: [String: String]) throws -> [String: String] { @@ -233,6 +236,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -281,10 +287,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given } @@ -996,6 +1002,461 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } +// MARK: - CourseAnalytics + +open class CourseAnalyticsMock: CourseAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func courseEnrollClicked(courseId: String, courseName: String) { + addInvocation(.m_courseEnrollClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_courseEnrollClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func courseEnrollSuccess(courseId: String, courseName: String) { + addInvocation(.m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func viewCourseClicked(courseId: String, courseName: String) { + addInvocation(.m_viewCourseClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_viewCourseClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func resumeCourseTapped(courseId: String, courseName: String, blockId: String) { + addInvocation(.m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`))) + let perform = methodPerformValue(.m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`))) as? (String, String, String) -> Void + perform?(`courseId`, `courseName`, `blockId`) + } + + open func sequentialClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + addInvocation(.m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) + let perform = methodPerformValue(.m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) as? (String, String, String, String) -> Void + perform?(`courseId`, `courseName`, `blockId`, `blockName`) + } + + open func verticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + addInvocation(.m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) + let perform = methodPerformValue(.m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) as? (String, String, String, String) -> Void + perform?(`courseId`, `courseName`, `blockId`, `blockName`) + } + + open func nextBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + addInvocation(.m_nextBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) + let perform = methodPerformValue(.m_nextBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) as? (String, String, String, String) -> Void + perform?(`courseId`, `courseName`, `blockId`, `blockName`) + } + + open func prevBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + addInvocation(.m_prevBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) + let perform = methodPerformValue(.m_prevBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) as? (String, String, String, String) -> Void + perform?(`courseId`, `courseName`, `blockId`, `blockName`) + } + + open func finishVerticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + addInvocation(.m_finishVerticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) + let perform = methodPerformValue(.m_finishVerticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) as? (String, String, String, String) -> Void + perform?(`courseId`, `courseName`, `blockId`, `blockName`) + } + + open func finishVerticalNextSectionClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + addInvocation(.m_finishVerticalNextSectionClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) + let perform = methodPerformValue(.m_finishVerticalNextSectionClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`), Parameter.value(`blockName`))) as? (String, String, String, String) -> Void + perform?(`courseId`, `courseName`, `blockId`, `blockName`) + } + + open func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) { + addInvocation(.m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func courseOutlineCourseTabClicked(courseId: String, courseName: String) { + addInvocation(.m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func courseOutlineVideosTabClicked(courseId: String, courseName: String) { + addInvocation(.m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) { + addInvocation(.m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) { + addInvocation(.m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + + fileprivate enum MethodType { + case m_courseEnrollClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_viewCourseClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(Parameter, Parameter, Parameter) + case m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter, Parameter, Parameter, Parameter) + case m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter, Parameter, Parameter, Parameter) + case m_nextBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter, Parameter, Parameter, Parameter) + case m_prevBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter, Parameter, Parameter, Parameter) + case m_finishVerticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter, Parameter, Parameter, Parameter) + case m_finishVerticalNextSectionClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter, Parameter, Parameter, Parameter) + case m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_courseEnrollClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_viewCourseClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_viewCourseClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(let lhsCourseid, let lhsCoursename, let lhsBlockid), .m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(let rhsCourseid, let rhsCoursename, let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockId")) + return Matcher.ComparisonResult(results) + + case (.m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let lhsCourseid, let lhsCoursename, let lhsBlockid, let lhsBlockname), .m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let rhsCourseid, let rhsCoursename, let rhsBlockid, let rhsBlockname)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockname, rhs: rhsBlockname, with: matcher), lhsBlockname, rhsBlockname, "blockName")) + return Matcher.ComparisonResult(results) + + case (.m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let lhsCourseid, let lhsCoursename, let lhsBlockid, let lhsBlockname), .m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let rhsCourseid, let rhsCoursename, let rhsBlockid, let rhsBlockname)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockname, rhs: rhsBlockname, with: matcher), lhsBlockname, rhsBlockname, "blockName")) + return Matcher.ComparisonResult(results) + + case (.m_nextBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let lhsCourseid, let lhsCoursename, let lhsBlockid, let lhsBlockname), .m_nextBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let rhsCourseid, let rhsCoursename, let rhsBlockid, let rhsBlockname)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockname, rhs: rhsBlockname, with: matcher), lhsBlockname, rhsBlockname, "blockName")) + return Matcher.ComparisonResult(results) + + case (.m_prevBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let lhsCourseid, let lhsCoursename, let lhsBlockid, let lhsBlockname), .m_prevBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let rhsCourseid, let rhsCoursename, let rhsBlockid, let rhsBlockname)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockname, rhs: rhsBlockname, with: matcher), lhsBlockname, rhsBlockname, "blockName")) + return Matcher.ComparisonResult(results) + + case (.m_finishVerticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let lhsCourseid, let lhsCoursename, let lhsBlockid, let lhsBlockname), .m_finishVerticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let rhsCourseid, let rhsCoursename, let rhsBlockid, let rhsBlockname)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockname, rhs: rhsBlockname, with: matcher), lhsBlockname, rhsBlockname, "blockName")) + return Matcher.ComparisonResult(results) + + case (.m_finishVerticalNextSectionClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let lhsCourseid, let lhsCoursename, let lhsBlockid, let lhsBlockname), .m_finishVerticalNextSectionClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(let rhsCourseid, let rhsCoursename, let rhsBlockid, let rhsBlockname)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockname, rhs: rhsBlockname, with: matcher), lhsBlockname, rhsBlockname, "blockName")) + return Matcher.ComparisonResult(results) + + case (.m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_viewCourseClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_nextBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_prevBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_finishVerticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_finishVerticalNextSectionClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_courseEnrollClicked__courseId_courseIdcourseName_courseName: return ".courseEnrollClicked(courseId:courseName:)" + case .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName: return ".courseEnrollSuccess(courseId:courseName:)" + case .m_viewCourseClicked__courseId_courseIdcourseName_courseName: return ".viewCourseClicked(courseId:courseName:)" + case .m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId: return ".resumeCourseTapped(courseId:courseName:blockId:)" + case .m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName: return ".sequentialClicked(courseId:courseName:blockId:blockName:)" + case .m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName: return ".verticalClicked(courseId:courseName:blockId:blockName:)" + case .m_nextBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName: return ".nextBlockClicked(courseId:courseName:blockId:blockName:)" + case .m_prevBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName: return ".prevBlockClicked(courseId:courseName:blockId:blockName:)" + case .m_finishVerticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName: return ".finishVerticalClicked(courseId:courseName:blockId:blockName:)" + case .m_finishVerticalNextSectionClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName: return ".finishVerticalNextSectionClicked(courseId:courseName:blockId:blockName:)" + case .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName: return ".finishVerticalBackToOutlineClicked(courseId:courseName:)" + case .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineCourseTabClicked(courseId:courseName:)" + case .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineVideosTabClicked(courseId:courseName:)" + case .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineDiscussionTabClicked(courseId:courseName:)" + case .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineHandoutsTabClicked(courseId:courseName:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func courseEnrollClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func courseEnrollSuccess(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func viewCourseClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_viewCourseClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func resumeCourseTapped(courseId: Parameter, courseName: Parameter, blockId: Parameter) -> Verify { return Verify(method: .m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(`courseId`, `courseName`, `blockId`))} + public static func sequentialClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter) -> Verify { return Verify(method: .m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`))} + public static func verticalClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter) -> Verify { return Verify(method: .m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`))} + public static func nextBlockClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter) -> Verify { return Verify(method: .m_nextBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`))} + public static func prevBlockClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter) -> Verify { return Verify(method: .m_prevBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`))} + public static func finishVerticalClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter) -> Verify { return Verify(method: .m_finishVerticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`))} + public static func finishVerticalNextSectionClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter) -> Verify { return Verify(method: .m_finishVerticalNextSectionClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`))} + public static func finishVerticalBackToOutlineClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func courseOutlineCourseTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func courseOutlineVideosTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func courseOutlineDiscussionTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func courseOutlineHandoutsTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func courseEnrollClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func courseEnrollSuccess(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func viewCourseClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_viewCourseClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func resumeCourseTapped(courseId: Parameter, courseName: Parameter, blockId: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { + return Perform(method: .m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(`courseId`, `courseName`, `blockId`), performs: perform) + } + public static func sequentialClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter, perform: @escaping (String, String, String, String) -> Void) -> Perform { + return Perform(method: .m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`), performs: perform) + } + public static func verticalClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter, perform: @escaping (String, String, String, String) -> Void) -> Perform { + return Perform(method: .m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`), performs: perform) + } + public static func nextBlockClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter, perform: @escaping (String, String, String, String) -> Void) -> Perform { + return Perform(method: .m_nextBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`), performs: perform) + } + public static func prevBlockClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter, perform: @escaping (String, String, String, String) -> Void) -> Perform { + return Perform(method: .m_prevBlockClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`), performs: perform) + } + public static func finishVerticalClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter, perform: @escaping (String, String, String, String) -> Void) -> Perform { + return Perform(method: .m_finishVerticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`), performs: perform) + } + public static func finishVerticalNextSectionClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter, perform: @escaping (String, String, String, String) -> Void) -> Perform { + return Perform(method: .m_finishVerticalNextSectionClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`), performs: perform) + } + public static func finishVerticalBackToOutlineClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func courseOutlineCourseTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func courseOutlineVideosTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func courseOutlineDiscussionTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func courseOutlineHandoutsTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + // MARK: - CourseInteractorProtocol open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { diff --git a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift index cea2c631b..bae62298c 100644 --- a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift @@ -18,6 +18,7 @@ final class CourseContainerViewModelTests: XCTestCase { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let connectivity = ConnectivityProtocolMock() @@ -27,6 +28,7 @@ final class CourseContainerViewModelTests: XCTestCase { interactor: interactor, authInteractor: authInteractor, router: router, + analytics: analytics, config: config, connectivity: connectivity, manager: DownloadManagerMock(), @@ -76,6 +78,7 @@ final class CourseContainerViewModelTests: XCTestCase { let childs = [chapter] let courseStructure = CourseStructure( + courseID: "1", id: "123", graded: true, completion: 0, @@ -116,6 +119,7 @@ final class CourseContainerViewModelTests: XCTestCase { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let connectivity = ConnectivityProtocolMock() @@ -125,6 +129,7 @@ final class CourseContainerViewModelTests: XCTestCase { interactor: interactor, authInteractor: authInteractor, router: router, + analytics: analytics, config: config, connectivity: connectivity, manager: DownloadManagerMock(), @@ -135,7 +140,8 @@ final class CourseContainerViewModelTests: XCTestCase { enrollmentEnd: nil ) - let courseStructure = CourseStructure(id: "123", + let courseStructure = CourseStructure(courseID: "1", + id: "123", graded: true, completion: 0, viewYouTubeUrl: "", @@ -166,6 +172,7 @@ final class CourseContainerViewModelTests: XCTestCase { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let connectivity = ConnectivityProtocolMock() @@ -175,6 +182,7 @@ final class CourseContainerViewModelTests: XCTestCase { interactor: interactor, authInteractor: authInteractor, router: router, + analytics: analytics, config: config, connectivity: connectivity, manager: DownloadManagerMock(), @@ -204,6 +212,7 @@ final class CourseContainerViewModelTests: XCTestCase { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let connectivity = ConnectivityProtocolMock() @@ -213,6 +222,7 @@ final class CourseContainerViewModelTests: XCTestCase { interactor: interactor, authInteractor: authInteractor, router: router, + analytics: analytics, config: config, connectivity: connectivity, manager: DownloadManagerMock(), @@ -239,6 +249,7 @@ final class CourseContainerViewModelTests: XCTestCase { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let connectivity = ConnectivityProtocolMock() @@ -248,6 +259,7 @@ final class CourseContainerViewModelTests: XCTestCase { interactor: interactor, authInteractor: authInteractor, router: router, + analytics: analytics, config: config, connectivity: connectivity, manager: DownloadManagerMock(), @@ -269,4 +281,42 @@ final class CourseContainerViewModelTests: XCTestCase { XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) XCTAssertNil(viewModel.courseStructure) } + + func testTabSelectedAnalytics() { + let interactor = CourseInteractorProtocolMock() + let authInteractor = AuthInteractorProtocolMock() + let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() + let config = ConfigMock() + let connectivity = ConnectivityProtocolMock() + + Given(connectivity, .isInternetAvaliable(getter: true)) + + let viewModel = CourseContainerViewModel( + interactor: interactor, + authInteractor: authInteractor, + router: router, + analytics: analytics, + config: config, + connectivity: connectivity, + manager: DownloadManagerMock(), + isActive: true, + courseStart: Date(), + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil + ) + + viewModel.trackSelectedTab(selection: .course, courseId: "1", courseName: "name") + Verify(analytics, .courseOutlineCourseTabClicked(courseId: .value("1"), courseName: .value("name"))) + + viewModel.trackSelectedTab(selection: .videos, courseId: "1", courseName: "name") + Verify(analytics, .courseOutlineVideosTabClicked(courseId: .value("1"), courseName: .value("name"))) + + viewModel.trackSelectedTab(selection: .discussion, courseId: "1", courseName: "name") + Verify(analytics, .courseOutlineDiscussionTabClicked(courseId: .value("1"), courseName: .value("name"))) + + viewModel.trackSelectedTab(selection: .handounds, courseId: "1", courseName: "name") + Verify(analytics, .courseOutlineHandoutsTabClicked(courseId: .value("1"), courseName: .value("name"))) + } } diff --git a/Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift b/Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift index 261a34bae..108fc104b 100644 --- a/Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift +++ b/Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift @@ -17,6 +17,7 @@ final class CourseDetailsViewModelTests: XCTestCase { func testGetCourseDetailSuccess() async throws { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -25,6 +26,7 @@ final class CourseDetailsViewModelTests: XCTestCase { let viewModel = CourseDetailsViewModel(interactor: interactor, router: router, + analytics: analytics, config: config, cssInjector: cssInjector, connectivity: connectivity) @@ -60,6 +62,7 @@ final class CourseDetailsViewModelTests: XCTestCase { func testGetCourseDetailSuccessOffline() async throws { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -68,6 +71,7 @@ final class CourseDetailsViewModelTests: XCTestCase { let viewModel = CourseDetailsViewModel(interactor: interactor, router: router, + analytics: analytics, config: config, cssInjector: cssInjector, connectivity: connectivity) @@ -102,6 +106,7 @@ final class CourseDetailsViewModelTests: XCTestCase { func testGetCourseDetailNoInternetError() async throws { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -110,6 +115,7 @@ final class CourseDetailsViewModelTests: XCTestCase { let viewModel = CourseDetailsViewModel(interactor: interactor, router: router, + analytics: analytics, config: config, cssInjector: cssInjector, connectivity: connectivity) @@ -131,6 +137,7 @@ final class CourseDetailsViewModelTests: XCTestCase { func testGetCourseDetailNoCacheError() async throws { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -139,6 +146,7 @@ final class CourseDetailsViewModelTests: XCTestCase { let viewModel = CourseDetailsViewModel(interactor: interactor, router: router, + analytics: analytics, config: config, cssInjector: cssInjector, connectivity: connectivity) @@ -158,6 +166,7 @@ final class CourseDetailsViewModelTests: XCTestCase { func testGetCourseDetailUnknownError() async throws { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -166,6 +175,7 @@ final class CourseDetailsViewModelTests: XCTestCase { let viewModel = CourseDetailsViewModel(interactor: interactor, router: router, + analytics: analytics, config: config, cssInjector: cssInjector, connectivity: connectivity) @@ -185,6 +195,7 @@ final class CourseDetailsViewModelTests: XCTestCase { func testEnrollToCourseSuccess() async throws { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -193,6 +204,7 @@ final class CourseDetailsViewModelTests: XCTestCase { let viewModel = CourseDetailsViewModel(interactor: interactor, router: router, + analytics: analytics, config: config, cssInjector: cssInjector, connectivity: connectivity) @@ -202,6 +214,8 @@ final class CourseDetailsViewModelTests: XCTestCase { await viewModel.enrollToCourse(id: "123") Verify(interactor, 1, .enrollToCourse(courseID: .any)) + Verify(analytics, .courseEnrollClicked(courseId: .any, courseName: .any)) + Verify(analytics, .courseEnrollSuccess(courseId: .any, courseName: .any)) XCTAssertFalse(viewModel.isShowProgress) XCTAssertNil(viewModel.errorMessage) @@ -211,6 +225,7 @@ final class CourseDetailsViewModelTests: XCTestCase { func testEnrollToCourseUnknownError() async throws { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -219,6 +234,7 @@ final class CourseDetailsViewModelTests: XCTestCase { let viewModel = CourseDetailsViewModel(interactor: interactor, router: router, + analytics: analytics, config: config, cssInjector: cssInjector, connectivity: connectivity) @@ -229,6 +245,7 @@ final class CourseDetailsViewModelTests: XCTestCase { await viewModel.enrollToCourse(id: "123") Verify(interactor, 1, .enrollToCourse(courseID: .any)) + Verify(analytics, .courseEnrollClicked(courseId: .any, courseName: .any)) XCTAssertFalse(viewModel.isShowProgress) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) @@ -238,6 +255,7 @@ final class CourseDetailsViewModelTests: XCTestCase { func testEnrollToCourseNoInternetError() async throws { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -246,6 +264,7 @@ final class CourseDetailsViewModelTests: XCTestCase { let viewModel = CourseDetailsViewModel(interactor: interactor, router: router, + analytics: analytics, config: config, cssInjector: cssInjector, connectivity: connectivity) @@ -267,6 +286,7 @@ final class CourseDetailsViewModelTests: XCTestCase { func testEnrollToCourseNoCacheError() async throws { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() + let analytics = CourseAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -275,6 +295,7 @@ final class CourseDetailsViewModelTests: XCTestCase { let viewModel = CourseDetailsViewModel(interactor: interactor, router: router, + analytics: analytics, config: config, cssInjector: cssInjector, connectivity: connectivity) diff --git a/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift index 2c01ac7e9..7c5be54e4 100644 --- a/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift @@ -104,16 +104,19 @@ final class CourseUnitViewModelTests: XCTestCase { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() + let analytics = CourseAnalyticsMock() let viewModel = CourseUnitViewModel(lessonID: "123", courseID: "456", id: "789", + courseName: "name", chapters: chapters, chapterIndex: 0, sequentialIndex: 0, verticalIndex: 0, interactor: interactor, router: router, + analytics: analytics, connectivity: connectivity, manager: DownloadManagerMock()) @@ -128,16 +131,19 @@ final class CourseUnitViewModelTests: XCTestCase { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() + let analytics = CourseAnalyticsMock() let viewModel = CourseUnitViewModel(lessonID: "123", courseID: "456", id: "789", + courseName: "name", chapters: chapters, chapterIndex: 0, sequentialIndex: 0, verticalIndex: 0, interactor: interactor, router: router, + analytics: analytics, connectivity: connectivity, manager: DownloadManagerMock()) @@ -157,16 +163,19 @@ final class CourseUnitViewModelTests: XCTestCase { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() + let analytics = CourseAnalyticsMock() let viewModel = CourseUnitViewModel(lessonID: "123", courseID: "456", id: "789", + courseName: "name", chapters: chapters, chapterIndex: 0, sequentialIndex: 0, verticalIndex: 0, interactor: interactor, router: router, + analytics: analytics, connectivity: connectivity, manager: DownloadManagerMock()) @@ -188,16 +197,19 @@ final class CourseUnitViewModelTests: XCTestCase { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() + let analytics = CourseAnalyticsMock() let viewModel = CourseUnitViewModel(lessonID: "123", courseID: "456", id: "789", + courseName: "name", chapters: chapters, chapterIndex: 0, sequentialIndex: 0, verticalIndex: 0, interactor: interactor, router: router, + analytics: analytics, connectivity: connectivity, manager: DownloadManagerMock()) @@ -218,16 +230,19 @@ final class CourseUnitViewModelTests: XCTestCase { let interactor = CourseInteractorProtocolMock() let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() + let analytics = CourseAnalyticsMock() let viewModel = CourseUnitViewModel(lessonID: "123", courseID: "456", id: "789", + courseName: "name", chapters: chapters, chapterIndex: 0, sequentialIndex: 0, verticalIndex: 0, interactor: interactor, router: router, + analytics: analytics, connectivity: connectivity, manager: DownloadManagerMock()) @@ -237,12 +252,15 @@ final class CourseUnitViewModelTests: XCTestCase { viewModel.select(move: .next) } + Verify(analytics, .nextBlockClicked(courseId: .any, courseName: .any, blockId: .any, blockName: .any)) + XCTAssertEqual(viewModel.index, 3) for _ in 0...CourseUnitViewModelTests.blocks.count - 1 { viewModel.select(move: .previous) } + Verify(analytics, .prevBlockClicked(courseId: .any, courseName: .any, blockId: .any, blockName: .any)) XCTAssertEqual(viewModel.index, 0) } diff --git a/Dashboard/Dashboard.xcodeproj/project.pbxproj b/Dashboard/Dashboard.xcodeproj/project.pbxproj index 6d116fcf2..b5103d7da 100644 --- a/Dashboard/Dashboard.xcodeproj/project.pbxproj +++ b/Dashboard/Dashboard.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 02A9A90B2978194100B55797 /* DashboardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A90A2978194100B55797 /* DashboardViewModelTests.swift */; }; 02A9A90C2978194100B55797 /* Dashboard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02EF39E728D89F560058F6BD /* Dashboard.framework */; platformFilter = ios; }; 02A9A92929781A4D00B55797 /* DashboardMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A92829781A4D00B55797 /* DashboardMock.generated.swift */; }; + 02F175332A4DABBF0019CD70 /* DashboardAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F175322A4DABBF0019CD70 /* DashboardAnalytics.swift */; }; 02F3BFE129252FCB0051930C /* DashboardRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFE029252FCB0051930C /* DashboardRouter.swift */; }; 02F6EF3D28D9EB8C00835477 /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = 02F6EF3C28D9EB8C00835477 /* swiftgen.yml */; }; 02F6EF4328D9ECC500835477 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 02F6EF4528D9ECC500835477 /* Localizable.strings */; }; @@ -50,6 +51,7 @@ 02A9A92829781A4D00B55797 /* DashboardMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardMock.generated.swift; sourceTree = ""; }; 02ED50CD29A64B9B008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02EF39E728D89F560058F6BD /* Dashboard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Dashboard.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 02F175322A4DABBF0019CD70 /* DashboardAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardAnalytics.swift; sourceTree = ""; }; 02F3BFE029252FCB0051930C /* DashboardRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardRouter.swift; sourceTree = ""; }; 02F6EF3C28D9EB8C00835477 /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; 02F6EF4428D9ECC500835477 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -179,6 +181,7 @@ 027DB33228D8BDBA002B6862 /* DashboardView.swift */, 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */, 02F3BFE029252FCB0051930C /* DashboardRouter.swift */, + 02F175322A4DABBF0019CD70 /* DashboardAnalytics.swift */, ); path = Presentation; sourceTree = ""; @@ -443,6 +446,7 @@ 02A48B1A295ACE3D0033D5E0 /* DashboardPersistence.swift in Sources */, 027DB33D28D8DB5E002B6862 /* DashboardRepository.swift in Sources */, 02A48B18295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld in Sources */, + 02F175332A4DABBF0019CD70 /* DashboardAnalytics.swift in Sources */, 027DB34528D8E9D2002B6862 /* DashboardViewModel.swift in Sources */, 027DB33328D8BDBA002B6862 /* DashboardView.swift in Sources */, 02F3BFE129252FCB0051930C /* DashboardRouter.swift in Sources */, diff --git a/Dashboard/Dashboard/Presentation/DashboardAnalytics.swift b/Dashboard/Dashboard/Presentation/DashboardAnalytics.swift new file mode 100644 index 000000000..d5edf28d5 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/DashboardAnalytics.swift @@ -0,0 +1,19 @@ +// +// DashboardAnalytics.swift +// Dashboard +// +// Created by  Stepanok Ivan on 29.06.2023. +// + +import Foundation + +//sourcery: AutoMockable +public protocol DashboardAnalytics { + func dashboardCourseClicked(courseID: String, courseName: String) +} + +#if DEBUG +class DashboardAnalyticsMock: DashboardAnalytics { + public func dashboardCourseClicked(courseID: String, courseName: String) {} +} +#endif diff --git a/Dashboard/Dashboard/Presentation/DashboardView.swift b/Dashboard/Dashboard/Presentation/DashboardView.swift index a47dca8e3..7ddc40b05 100644 --- a/Dashboard/Dashboard/Presentation/DashboardView.swift +++ b/Dashboard/Dashboard/Presentation/DashboardView.swift @@ -72,6 +72,7 @@ public struct DashboardView: View { } } .onTapGesture { + viewModel.dashboardCourseClicked(courseID: course.courseID, courseName: course.name) router.showCourseScreens( courseID: course.courseID, isActive: course.isActive, @@ -132,7 +133,8 @@ struct DashboardView_Previews: PreviewProvider { static var previews: some View { let vm = DashboardViewModel( interactor: DashboardInteractor.mock, - connectivity: Connectivity() + connectivity: Connectivity(), + analytics: DashboardAnalyticsMock() ) let router = DashboardRouterMock() diff --git a/Dashboard/Dashboard/Presentation/DashboardViewModel.swift b/Dashboard/Dashboard/Presentation/DashboardViewModel.swift index 0ffe9a547..18de4b976 100644 --- a/Dashboard/Dashboard/Presentation/DashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/DashboardViewModel.swift @@ -26,14 +26,17 @@ public class DashboardViewModel: ObservableObject { } } + let connectivity: ConnectivityProtocol private let interactor: DashboardInteractorProtocol - public let connectivity: ConnectivityProtocol - + private let analytics: DashboardAnalytics private var onCourseEnrolledCancellable: AnyCancellable? - public init(interactor: DashboardInteractorProtocol, connectivity: ConnectivityProtocol) { + public init(interactor: DashboardInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DashboardAnalytics) { self.interactor = interactor self.connectivity = connectivity + self.analytics = analytics onCourseEnrolledCancellable = NotificationCenter.default .publisher(for: .onCourseEnrolled) @@ -91,4 +94,8 @@ public class DashboardViewModel: ObservableObject { } } } + + func dashboardCourseClicked(courseID: String, courseName: String) { + analytics.dashboardCourseClicked(courseID: courseID, courseName: courseName) + } } diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index 548809f83..551f38c47 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -121,17 +121,20 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws { + open func registerUser(fields: [String: String]) throws -> User { addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void perform?(`fields`) + var __value: User do { - _ = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() as Void + __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() } catch MockError.notStubed { - // do nothing + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") } catch { throw error } + return __value } open func validateRegistrationFields(fields: [String: String]) throws -> [String: String] { @@ -233,6 +236,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -281,10 +287,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given } @@ -996,6 +1002,181 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } +// MARK: - DashboardAnalytics + +open class DashboardAnalyticsMock: DashboardAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func dashboardCourseClicked(courseID: String, courseName: String) { + addInvocation(.m_dashboardCourseClicked__courseID_courseIDcourseName_courseName(Parameter.value(`courseID`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_dashboardCourseClicked__courseID_courseIDcourseName_courseName(Parameter.value(`courseID`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseID`, `courseName`) + } + + + fileprivate enum MethodType { + case m_dashboardCourseClicked__courseID_courseIDcourseName_courseName(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_dashboardCourseClicked__courseID_courseIDcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_dashboardCourseClicked__courseID_courseIDcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + } + } + + func intValue() -> Int { + switch self { + case let .m_dashboardCourseClicked__courseID_courseIDcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_dashboardCourseClicked__courseID_courseIDcourseName_courseName: return ".dashboardCourseClicked(courseID:courseName:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func dashboardCourseClicked(courseID: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_dashboardCourseClicked__courseID_courseIDcourseName_courseName(`courseID`, `courseName`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func dashboardCourseClicked(courseID: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_dashboardCourseClicked__courseID_courseIDcourseName_courseName(`courseID`, `courseName`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + // MARK: - DashboardInteractorProtocol open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { diff --git a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift index 8fda4d939..d3261b52d 100644 --- a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift +++ b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift @@ -17,7 +17,8 @@ final class DashboardViewModelTests: XCTestCase { func testGetMyCoursesSuccess() async throws { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity) + let analytics = DashboardAnalyticsMock() + let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) let items = [ CourseItem(name: "Test", @@ -61,7 +62,8 @@ final class DashboardViewModelTests: XCTestCase { func testGetMyCoursesOfflineSuccess() async throws { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity) + let analytics = DashboardAnalyticsMock() + let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) let items = [ CourseItem(name: "Test", @@ -105,7 +107,8 @@ final class DashboardViewModelTests: XCTestCase { func testGetMyCoursesNoCacheError() async throws { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity) + let analytics = DashboardAnalyticsMock() + let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .getMyCourses(page: .any, willThrow: NoCachedDataError()) ) @@ -122,7 +125,8 @@ final class DashboardViewModelTests: XCTestCase { func testGetMyCoursesUnknownError() async throws { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity) + let analytics = DashboardAnalyticsMock() + let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .getMyCourses(page: .any, willThrow: NSError()) ) diff --git a/Discovery/Discovery.xcodeproj/project.pbxproj b/Discovery/Discovery.xcodeproj/project.pbxproj index c4cf2bbd2..b6213f2ff 100644 --- a/Discovery/Discovery.xcodeproj/project.pbxproj +++ b/Discovery/Discovery.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 02EF39D128D867690058F6BD /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = 02EF39D028D867690058F6BD /* swiftgen.yml */; }; 02EF39D728D86A380058F6BD /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 02EF39D928D86A380058F6BD /* Localizable.strings */; }; 02EF39DC28D86BEF0058F6BD /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EF39DB28D86BEF0058F6BD /* Strings.swift */; }; + 02F1752F2A4DA3B60019CD70 /* DiscoveryAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F1752E2A4DA3B60019CD70 /* DiscoveryAnalytics.swift */; }; 02F3BFDF29252F2F0051930C /* DiscoveryRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFDE29252F2F0051930C /* DiscoveryRouter.swift */; }; 072787AD28D34D15002E9142 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072787AC28D34D15002E9142 /* Core.framework */; }; 072787B428D34D91002E9142 /* DiscoveryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072787B328D34D91002E9142 /* DiscoveryView.swift */; }; @@ -55,6 +56,7 @@ 02EF39D028D867690058F6BD /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; 02EF39D828D86A380058F6BD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 02EF39DB28D86BEF0058F6BD /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; + 02F1752E2A4DA3B60019CD70 /* DiscoveryAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryAnalytics.swift; sourceTree = ""; }; 02F3BFDE29252F2F0051930C /* DiscoveryRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryRouter.swift; sourceTree = ""; }; 0692409931272CDA39B10321 /* Pods-App-Discovery.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Discovery/Pods-App-Discovery.releasestage.xcconfig"; sourceTree = ""; }; 0727879928D34C03002E9142 /* Discovery.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Discovery.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -165,6 +167,7 @@ CFC849422996A5150055E497 /* SearchView.swift */, CFC849442996A52A0055E497 /* SearchViewModel.swift */, 02F3BFDE29252F2F0051930C /* DiscoveryRouter.swift */, + 02F1752E2A4DA3B60019CD70 /* DiscoveryAnalytics.swift */, ); path = Presentation; sourceTree = ""; @@ -466,6 +469,7 @@ 029737422949FB3B0051696B /* DiscoveryPersistence.swift in Sources */, 0284DC0328D4922900830893 /* DiscoveryRepository.swift in Sources */, 02EF39DC28D86BEF0058F6BD /* Strings.swift in Sources */, + 02F1752F2A4DA3B60019CD70 /* DiscoveryAnalytics.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift b/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift new file mode 100644 index 000000000..f86ef6341 --- /dev/null +++ b/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift @@ -0,0 +1,23 @@ +// +// DiscoveryAnalytics.swift +// Discovery +// +// Created by  Stepanok Ivan on 29.06.2023. +// + +import Foundation + +//sourcery: AutoMockable +public protocol DiscoveryAnalytics { + func discoverySearchBarClicked() + func discoveryCoursesSearch(label: String, coursesCount: Int) + func discoveryCourseClicked(courseID: String, courseName: String) +} + +#if DEBUG +class DiscoveryAnalyticsMock: DiscoveryAnalytics { + public func discoverySearchBarClicked() {} + public func discoveryCoursesSearch(label: String, coursesCount: Int) {} + public func discoveryCourseClicked(courseID: String, courseName: String) {} +} +#endif diff --git a/Discovery/Discovery/Presentation/DiscoveryView.swift b/Discovery/Discovery/Presentation/DiscoveryView.swift index 8b5af1c7a..043170fc7 100644 --- a/Discovery/Discovery/Presentation/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/DiscoveryView.swift @@ -53,6 +53,7 @@ public struct DiscoveryView: View { } .onTapGesture { router.showDiscoverySearch() + viewModel.discoverySearchBarClicked() } .frame(minHeight: 48) .frame(maxWidth: 532) @@ -66,6 +67,7 @@ public struct DiscoveryView: View { .fill(CoreAssets.textInputUnfocusedStroke.swiftUIColor) ).onTapGesture { router.showDiscoverySearch() + viewModel.discoverySearchBarClicked() } .padding(.horizontal, 24) .padding(.bottom, 20) @@ -97,6 +99,7 @@ public struct DiscoveryView: View { } } .onTapGesture { + viewModel.discoveryCourseClicked(courseID: course.courseID, courseName: course.name) router.showCourseDetais( courseID: course.courseID, title: course.name @@ -150,7 +153,8 @@ public struct DiscoveryView: View { #if DEBUG struct DiscoveryView_Previews: PreviewProvider { static var previews: some View { - let vm = DiscoveryViewModel(interactor: DiscoveryInteractor.mock, connectivity: Connectivity()) + let vm = DiscoveryViewModel(interactor: DiscoveryInteractor.mock, connectivity: Connectivity(), + analytics: DiscoveryAnalyticsMock()) let router = DiscoveryRouterMock() DiscoveryView(viewModel: vm, router: router) diff --git a/Discovery/Discovery/Presentation/DiscoveryViewModel.swift b/Discovery/Discovery/Presentation/DiscoveryViewModel.swift index 53e0f7e32..c99e2e3f8 100644 --- a/Discovery/Discovery/Presentation/DiscoveryViewModel.swift +++ b/Discovery/Discovery/Presentation/DiscoveryViewModel.swift @@ -26,12 +26,16 @@ public class DiscoveryViewModel: ObservableObject { } } - private let interactor: DiscoveryInteractorProtocol let connectivity: ConnectivityProtocol + private let interactor: DiscoveryInteractorProtocol + private let analytics: DiscoveryAnalytics - public init(interactor: DiscoveryInteractorProtocol, connectivity: ConnectivityProtocol) { + public init(interactor: DiscoveryInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DiscoveryAnalytics) { self.interactor = interactor self.connectivity = connectivity + self.analytics = analytics } @MainActor @@ -76,4 +80,11 @@ public class DiscoveryViewModel: ObservableObject { } } + func discoveryCourseClicked(courseID: String, courseName: String) { + analytics.discoveryCourseClicked(courseID: courseID, courseName: courseName) + } + + func discoverySearchBarClicked() { + analytics.discoverySearchBarClicked() + } } diff --git a/Discovery/Discovery/Presentation/SearchView.swift b/Discovery/Discovery/Presentation/SearchView.swift index aecd40f5a..b330d8c5e 100644 --- a/Discovery/Discovery/Presentation/SearchView.swift +++ b/Discovery/Discovery/Presentation/SearchView.swift @@ -13,6 +13,7 @@ public struct SearchView: View { @ObservedObject private var viewModel: SearchViewModel @State private var animated: Bool = false + @State private var becomeFirstResponderRunOnce = false public init(viewModel: SearchViewModel) { self.viewModel = viewModel @@ -47,9 +48,12 @@ public struct SearchView: View { viewModel.isSearchActive = editing } ) - .introspectTextField { textField in - textField.becomeFirstResponder() - } + .introspect(.textField, on: .iOS(.v14, .v15, .v16, .v17), customize: { textField in + if !becomeFirstResponderRunOnce { + textField.becomeFirstResponder() + self.becomeFirstResponderRunOnce = true + } + }) .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() if !viewModel.searchText.trimmingCharacters(in: .whitespaces).isEmpty { @@ -192,6 +196,7 @@ struct SearchView_Previews: PreviewProvider { interactor: DiscoveryInteractor.mock, connectivity: Connectivity(), router: router, + analytics: DiscoveryAnalyticsMock(), debounce: .searchDebounce ) diff --git a/Discovery/Discovery/Presentation/SearchViewModel.swift b/Discovery/Discovery/Presentation/SearchViewModel.swift index 68f8d61bc..8f0c6ff1c 100644 --- a/Discovery/Discovery/Presentation/SearchViewModel.swift +++ b/Discovery/Discovery/Presentation/SearchViewModel.swift @@ -31,17 +31,20 @@ public class SearchViewModel: ObservableObject { } let router: DiscoveryRouter + let analytics: DiscoveryAnalytics private let interactor: DiscoveryInteractorProtocol let connectivity: ConnectivityProtocol public init(interactor: DiscoveryInteractorProtocol, connectivity: ConnectivityProtocol, router: DiscoveryRouter, + analytics: DiscoveryAnalytics, debounce: Debounce ) { self.interactor = interactor self.connectivity = connectivity self.router = router + self.analytics = analytics self.debounce = debounce $searchText @@ -90,22 +93,26 @@ public class SearchViewModel: ObservableObject { if !searchTerm.trimmingCharacters(in: .whitespaces).isEmpty { var results: [CourseItem] = [] await results = try interactor.search(page: page, searchTerm: searchTerm) + if results.isEmpty { searchResults.removeAll() fetchInProgress = false return } - + if page == 1 { searchResults = results } else { searchResults += results } - + if !searchResults.isEmpty { self.nextPage += 1 totalPages = results[0].numPages } + + analytics.discoveryCoursesSearch(label: searchTerm, + coursesCount: searchResults.first?.coursesCount ?? 0) } fetchInProgress = false diff --git a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift index c76e3205c..c0c6c1793 100644 --- a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift +++ b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift @@ -121,17 +121,20 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws { + open func registerUser(fields: [String: String]) throws -> User { addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void perform?(`fields`) + var __value: User do { - _ = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() as Void + __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() } catch MockError.notStubed { - // do nothing + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") } catch { throw error } + return __value } open func validateRegistrationFields(fields: [String: String]) throws -> [String: String] { @@ -233,6 +236,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -281,10 +287,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given } @@ -996,6 +1002,216 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } +// MARK: - DiscoveryAnalytics + +open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func discoverySearchBarClicked() { + addInvocation(.m_discoverySearchBarClicked) + let perform = methodPerformValue(.m_discoverySearchBarClicked) as? () -> Void + perform?() + } + + open func discoveryCoursesSearch(label: String, coursesCount: Int) { + addInvocation(.m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(Parameter.value(`label`), Parameter.value(`coursesCount`))) + let perform = methodPerformValue(.m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(Parameter.value(`label`), Parameter.value(`coursesCount`))) as? (String, Int) -> Void + perform?(`label`, `coursesCount`) + } + + open func discoveryCourseClicked(courseID: String, courseName: String) { + addInvocation(.m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(Parameter.value(`courseID`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(Parameter.value(`courseID`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseID`, `courseName`) + } + + + fileprivate enum MethodType { + case m_discoverySearchBarClicked + case m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(Parameter, Parameter) + case m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_discoverySearchBarClicked, .m_discoverySearchBarClicked): return .match + + case (.m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(let lhsLabel, let lhsCoursescount), .m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(let rhsLabel, let rhsCoursescount)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsLabel, rhs: rhsLabel, with: matcher), lhsLabel, rhsLabel, "label")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursescount, rhs: rhsCoursescount, with: matcher), lhsCoursescount, rhsCoursescount, "coursesCount")) + return Matcher.ComparisonResult(results) + + case (.m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .m_discoverySearchBarClicked: return 0 + case let .m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(p0, p1): return p0.intValue + p1.intValue + case let .m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_discoverySearchBarClicked: return ".discoverySearchBarClicked()" + case .m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount: return ".discoveryCoursesSearch(label:coursesCount:)" + case .m_discoveryCourseClicked__courseID_courseIDcourseName_courseName: return ".discoveryCourseClicked(courseID:courseName:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func discoverySearchBarClicked() -> Verify { return Verify(method: .m_discoverySearchBarClicked)} + public static func discoveryCoursesSearch(label: Parameter, coursesCount: Parameter) -> Verify { return Verify(method: .m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(`label`, `coursesCount`))} + public static func discoveryCourseClicked(courseID: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(`courseID`, `courseName`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func discoverySearchBarClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_discoverySearchBarClicked, performs: perform) + } + public static func discoveryCoursesSearch(label: Parameter, coursesCount: Parameter, perform: @escaping (String, Int) -> Void) -> Perform { + return Perform(method: .m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(`label`, `coursesCount`), performs: perform) + } + public static func discoveryCourseClicked(courseID: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(`courseID`, `courseName`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + // MARK: - DiscoveryInteractorProtocol open class DiscoveryInteractorProtocolMock: DiscoveryInteractorProtocol, Mock { diff --git a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift index 6a5a41992..a31924505 100644 --- a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift @@ -25,7 +25,8 @@ final class DiscoveryViewModelTests: XCTestCase { func testGetDiscoveryCourses() async throws { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() - let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity) + let analytics = DiscoveryAnalyticsMock() + let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) let items = [ CourseItem(name: "Test", @@ -69,7 +70,8 @@ final class DiscoveryViewModelTests: XCTestCase { func testDiscoverySuccess() async throws { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() - let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity) + let analytics = DiscoveryAnalyticsMock() + let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) let items = [ CourseItem(name: "Test", @@ -112,7 +114,8 @@ final class DiscoveryViewModelTests: XCTestCase { func testDiscoveryOfflineSuccess() async throws { let interactor = DiscoveryInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() - let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity) + let analytics = DiscoveryAnalyticsMock() + let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) let items = [ CourseItem(name: "Test", @@ -157,7 +160,8 @@ final class DiscoveryViewModelTests: XCTestCase { func testDiscoveryNoInternetError() async throws { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() - let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity) + let analytics = DiscoveryAnalyticsMock() + let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -175,7 +179,8 @@ final class DiscoveryViewModelTests: XCTestCase { func testDiscoveryUnknownError() async throws { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() - let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity) + let analytics = DiscoveryAnalyticsMock() + let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) let noInternetError = AFError.sessionInvalidated(error: NSError()) diff --git a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift index 86ddc8ff4..3baf45321 100644 --- a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift @@ -25,11 +25,13 @@ final class SearchViewModelTests: XCTestCase { func testSearchSuccess() async throws { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() + let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( interactor: interactor, connectivity: connectivity, router: router, + analytics: analytics, debounce: .test ) @@ -72,6 +74,7 @@ final class SearchViewModelTests: XCTestCase { wait(for: [exp], timeout: 1) Verify(interactor, .search(page: 1, searchTerm: .any)) + Verify(analytics, .discoveryCoursesSearch(label: .any, coursesCount: .any)) XCTAssertFalse(viewModel.showError) XCTAssertFalse(viewModel.fetchInProgress) @@ -80,11 +83,13 @@ final class SearchViewModelTests: XCTestCase { func testSearchEmptyQuerySuccess() async throws { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() + let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( interactor: interactor, connectivity: connectivity, router: router, + analytics: analytics, debounce: .test ) @@ -106,11 +111,13 @@ final class SearchViewModelTests: XCTestCase { func testSearchNoInternetError() async throws { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() + let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( interactor: interactor, connectivity: connectivity, router: router, + analytics: analytics, debounce: .test ) @@ -137,11 +144,13 @@ final class SearchViewModelTests: XCTestCase { func testSearchUnknownError() async throws { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() + let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( interactor: interactor, connectivity: connectivity, router: router, + analytics: analytics, debounce: .test ) diff --git a/Discussion/Discussion.xcodeproj/project.pbxproj b/Discussion/Discussion.xcodeproj/project.pbxproj index f8a61da82..8f4b4c86b 100644 --- a/Discussion/Discussion.xcodeproj/project.pbxproj +++ b/Discussion/Discussion.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ 02D1267628F76F5D00C8E689 /* DiscussionTopicsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D1267528F76F5D00C8E689 /* DiscussionTopicsView.swift */; }; 02D1267828F76FF200C8E689 /* DiscussionTopicsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D1267728F76FF200C8E689 /* DiscussionTopicsViewModel.swift */; }; 02E4F18129A8C2FD00F31684 /* DiscussionSearchTopicsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4F18029A8C2FD00F31684 /* DiscussionSearchTopicsViewModelTests.swift */; }; + 02F175392A4DD5AB0019CD70 /* DiscussionAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F175382A4DD5AA0019CD70 /* DiscussionAnalytics.swift */; }; 02F28A5E28FF23E700AFDE1B /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F28A5D28FF23E700AFDE1B /* ThreadView.swift */; }; 02F28A6028FF23F300AFDE1B /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F28A5F28FF23F300AFDE1B /* ThreadViewModel.swift */; }; 02F3BFE32925302A0051930C /* DiscussionRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFE22925302A0051930C /* DiscussionRouter.swift */; }; @@ -108,6 +109,7 @@ 02E4F18029A8C2FD00F31684 /* DiscussionSearchTopicsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionSearchTopicsViewModelTests.swift; sourceTree = ""; }; 02ED50D029A64BBF008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02ED50D129A64BBF008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; + 02F175382A4DD5AA0019CD70 /* DiscussionAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionAnalytics.swift; sourceTree = ""; }; 02F28A5D28FF23E700AFDE1B /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = ""; }; 02F28A5F28FF23F300AFDE1B /* ThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewModel.swift; sourceTree = ""; }; 02F3BFE22925302A0051930C /* DiscussionRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionRouter.swift; sourceTree = ""; }; @@ -245,6 +247,7 @@ 0282DA5C28F89397003C3F07 /* DiscussionTopics */, 02CF2C8D291FA76E00FC1596 /* CheckBoxView.swift */, 02F3BFE22925302A0051930C /* DiscussionRouter.swift */, + 02F175382A4DD5AA0019CD70 /* DiscussionAnalytics.swift */, ); path = Presentation; sourceTree = ""; @@ -680,6 +683,7 @@ 029B78F5292519910097ACD8 /* ResponsesViewModel.swift in Sources */, 0282DA6128F893E9003C3F07 /* PostsViewModel.swift in Sources */, 0766DFC4299AA2C200EBEF6A /* Post.swift in Sources */, + 02F175392A4DD5AB0019CD70 /* DiscussionAnalytics.swift in Sources */, 021078E929A50BA30000938D /* DiscussionSearchTopicsViewModel.swift in Sources */, 02F3BFEB2926A5B50051930C /* Data_CommentsResponse.swift in Sources */, 075DBBB329267D1D00E56134 /* PostState.swift in Sources */, diff --git a/Discussion/Discussion/Presentation/DiscussionAnalytics.swift b/Discussion/Discussion/Presentation/DiscussionAnalytics.swift new file mode 100644 index 000000000..969994b4c --- /dev/null +++ b/Discussion/Discussion/Presentation/DiscussionAnalytics.swift @@ -0,0 +1,23 @@ +// +// DiscussionAnalytics.swift +// Discussion +// +// Created by  Stepanok Ivan on 29.06.2023. +// + +import Foundation + +//sourcery: AutoMockable +public protocol DiscussionAnalytics { + func discussionAllPostsClicked(courseId: String, courseName: String) + func discussionFollowingClicked(courseId: String, courseName: String) + func discussionTopicClicked(courseId: String, courseName: String, topicId: String, topicName: String) +} + +#if DEBUG +class DiscussionAnalyticsMock: DiscussionAnalytics { + public func discussionAllPostsClicked(courseId: String, courseName: String) {} + public func discussionFollowingClicked(courseId: String, courseName: String) {} + public func discussionTopicClicked(courseId: String, courseName: String, topicId: String, topicName: String) {} +} +#endif diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift index 281aee8e8..e7f925c02 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift @@ -12,6 +12,7 @@ public struct DiscussionSearchTopicsView: View { @ObservedObject private var viewModel: DiscussionSearchTopicsViewModel @State private var animated: Bool = false + @State private var becomeFirstResponderRunOnce = false public init(viewModel: DiscussionSearchTopicsViewModel) { self.viewModel = viewModel @@ -44,9 +45,12 @@ public struct DiscussionSearchTopicsView: View { viewModel.isSearchActive = editing } ) - .introspectTextField { textField in - textField.becomeFirstResponder() - } + .introspect(.textField, on: .iOS(.v14, .v15, .v16, .v17), customize: { textField in + if !becomeFirstResponderRunOnce { + textField.becomeFirstResponder() + self.becomeFirstResponderRunOnce = true + } + }) .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() if !viewModel.searchText.trimmingCharacters(in: .whitespaces).isEmpty { diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift index d6cc6f64c..acb81523d 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift @@ -161,8 +161,10 @@ public struct DiscussionTopicsView: View { struct DiscussionView_Previews: PreviewProvider { static var previews: some View { let vm = DiscussionTopicsViewModel( + title: "Course name", interactor: DiscussionInteractor.mock, router: DiscussionRouterMock(), + analytics: DiscussionAnalyticsMock(), config: ConfigMock()) let router = DiscussionRouterMock() diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift index ecbce5498..9364f4d6b 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import Core +import FirebaseCrashlytics public class DiscussionTopicsViewModel: ObservableObject { @@ -16,6 +17,7 @@ public class DiscussionTopicsViewModel: ObservableObject { @Published var showError: Bool = false @Published var discussionTopics: [DiscussionTopic]? @Published var courseID: String = "" + private var title: String var errorMessage: String? { didSet { @@ -25,13 +27,20 @@ public class DiscussionTopicsViewModel: ObservableObject { } } - public let interactor: DiscussionInteractorProtocol - public let router: DiscussionRouter - public let config: Config + let interactor: DiscussionInteractorProtocol + let router: DiscussionRouter + let analytics: DiscussionAnalytics + let config: Config - public init(interactor: DiscussionInteractorProtocol, router: DiscussionRouter, config: Config) { + public init(title: String, + interactor: DiscussionInteractorProtocol, + router: DiscussionRouter, + analytics: DiscussionAnalytics, + config: Config) { + self.title = title self.interactor = interactor self.router = router + self.analytics = analytics self.config = config } @@ -40,6 +49,8 @@ public class DiscussionTopicsViewModel: ObservableObject { DiscussionTopic( name: DiscussionLocalization.Topics.allPosts, action: { + self.analytics.discussionAllPostsClicked(courseId: self.courseID, + courseName: self.title) self.router.showThreads( courseID: self.courseID, topics: topics ?? Topics(coursewareTopics: [], nonCoursewareTopics: []), @@ -50,6 +61,8 @@ public class DiscussionTopicsViewModel: ObservableObject { ), DiscussionTopic( name: DiscussionLocalization.Topics.postImFollowing, action: { + self.analytics.discussionFollowingClicked(courseId: self.courseID, + courseName: self.title) self.router.showThreads( courseID: self.courseID, topics: topics ?? Topics(coursewareTopics: [], nonCoursewareTopics: []), @@ -65,6 +78,12 @@ public class DiscussionTopicsViewModel: ObservableObject { DiscussionTopic( name: t.name, action: { + self.analytics.discussionTopicClicked( + courseId: self.courseID, + courseName: self.title, + topicId: t.id, + topicName: t.name + ) self.router.showThreads( courseID: self.courseID, topics: topics, @@ -79,6 +98,12 @@ public class DiscussionTopicsViewModel: ObservableObject { DiscussionTopic( name: children.name, action: { + self.analytics.discussionTopicClicked( + courseId: self.courseID, + courseName: self.title, + topicId: t.id, + topicName: t.name + ) self.router.showThreads( courseID: self.courseID, topics: topics, @@ -102,6 +127,12 @@ public class DiscussionTopicsViewModel: ObservableObject { DiscussionTopic( name: child.name, action: { + self.analytics.discussionTopicClicked( + courseId: self.courseID, + courseName: self.title, + topicId: child.id, + topicName: child.name + ) self.router.showThreads( courseID: self.courseID, topics: topics, diff --git a/Discussion/DiscussionTests/DiscussionMock.generated.swift b/Discussion/DiscussionTests/DiscussionMock.generated.swift index 868fa3963..7533d76d0 100644 --- a/Discussion/DiscussionTests/DiscussionMock.generated.swift +++ b/Discussion/DiscussionTests/DiscussionMock.generated.swift @@ -121,17 +121,20 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws { + open func registerUser(fields: [String: String]) throws -> User { addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void perform?(`fields`) + var __value: User do { - _ = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() as Void + __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() } catch MockError.notStubed { - // do nothing + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") } catch { throw error } + return __value } open func validateRegistrationFields(fields: [String: String]) throws -> [String: String] { @@ -233,6 +236,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -281,10 +287,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given } @@ -996,6 +1002,222 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } +// MARK: - DiscussionAnalytics + +open class DiscussionAnalyticsMock: DiscussionAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func discussionAllPostsClicked(courseId: String, courseName: String) { + addInvocation(.m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func discussionFollowingClicked(courseId: String, courseName: String) { + addInvocation(.m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func discussionTopicClicked(courseId: String, courseName: String, topicId: String, topicName: String) { + addInvocation(.m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`topicId`), Parameter.value(`topicName`))) + let perform = methodPerformValue(.m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`topicId`), Parameter.value(`topicName`))) as? (String, String, String, String) -> Void + perform?(`courseId`, `courseName`, `topicId`, `topicName`) + } + + + fileprivate enum MethodType { + case m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(Parameter, Parameter, Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(let lhsCourseid, let lhsCoursename, let lhsTopicid, let lhsTopicname), .m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(let rhsCourseid, let rhsCoursename, let rhsTopicid, let rhsTopicname)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTopicid, rhs: rhsTopicid, with: matcher), lhsTopicid, rhsTopicid, "topicId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTopicname, rhs: rhsTopicname, with: matcher), lhsTopicname, rhsTopicname, "topicName")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + } + } + func assertionName() -> String { + switch self { + case .m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName: return ".discussionAllPostsClicked(courseId:courseName:)" + case .m_discussionFollowingClicked__courseId_courseIdcourseName_courseName: return ".discussionFollowingClicked(courseId:courseName:)" + case .m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName: return ".discussionTopicClicked(courseId:courseName:topicId:topicName:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func discussionAllPostsClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func discussionFollowingClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func discussionTopicClicked(courseId: Parameter, courseName: Parameter, topicId: Parameter, topicName: Parameter) -> Verify { return Verify(method: .m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(`courseId`, `courseName`, `topicId`, `topicName`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func discussionAllPostsClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_discussionAllPostsClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func discussionFollowingClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_discussionFollowingClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func discussionTopicClicked(courseId: Parameter, courseName: Parameter, topicId: Parameter, topicName: Parameter, perform: @escaping (String, String, String, String) -> Void) -> Perform { + return Perform(method: .m_discussionTopicClicked__courseId_courseIdcourseName_courseNametopicId_topicIdtopicName_topicName(`courseId`, `courseName`, `topicId`, `topicName`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + // MARK: - DiscussionInteractorProtocol open class DiscussionInteractorProtocolMock: DiscussionInteractorProtocol, Mock { diff --git a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift index 8b62b4049..e411e7fcd 100644 --- a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift @@ -39,8 +39,13 @@ final class DiscussionTopicsViewModelTests: XCTestCase { func testGetTopicsSuccess() async throws { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() + let analytics = DiscussionAnalyticsMock() let config = ConfigMock() - let viewModel = DiscussionTopicsViewModel(interactor: interactor, router: router, config: config) + let viewModel = DiscussionTopicsViewModel(title: "", + interactor: interactor, + router: router, + analytics: analytics, + config: config) Given(interactor, .getTopics(courseID: .any, willReturn: topics)) @@ -58,8 +63,13 @@ final class DiscussionTopicsViewModelTests: XCTestCase { func testGetTopicsNoInternetError() async throws { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() + let analytics = DiscussionAnalyticsMock() let config = ConfigMock() - let viewModel = DiscussionTopicsViewModel(interactor: interactor, router: router, config: config) + let viewModel = DiscussionTopicsViewModel(title: "", + interactor: interactor, + router: router, + analytics: analytics, + config: config) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -79,9 +89,14 @@ final class DiscussionTopicsViewModelTests: XCTestCase { func testGetTopicsUnknownError() async throws { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() + let analytics = DiscussionAnalyticsMock() let config = ConfigMock() - let viewModel = DiscussionTopicsViewModel(interactor: interactor, router: router, config: config) - + let viewModel = DiscussionTopicsViewModel(title: "", + interactor: interactor, + router: router, + analytics: analytics, + config: config) + Given(interactor, .getTopics(courseID: .any, willThrow: NSError())) await viewModel.getTopics(courseID: "1") diff --git a/NewEdX.xcodeproj/project.pbxproj b/NewEdX.xcodeproj/project.pbxproj index e102a8e84..90dfa72b7 100644 --- a/NewEdX.xcodeproj/project.pbxproj +++ b/NewEdX.xcodeproj/project.pbxproj @@ -16,8 +16,10 @@ 025DE1A528DB4DAE0053E0F4 /* Profile.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 025DE1A328DB4DAE0053E0F4 /* Profile.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 027DB33028D8A063002B6862 /* Dashboard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 027DB32F28D8A063002B6862 /* Dashboard.framework */; }; 027DB33128D8A063002B6862 /* Dashboard.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 027DB32F28D8A063002B6862 /* Dashboard.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0298DF302A4EF7230023A257 /* AnalyticsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0298DF2F2A4EF7230023A257 /* AnalyticsManager.swift */; }; 02ED50D429A6554E008341CD /* сountries.json in Resources */ = {isa = PBXBuildFile; fileRef = 02ED50D629A6554E008341CD /* сountries.json */; }; 02ED50D829A66007008341CD /* languages.json in Resources */ = {isa = PBXBuildFile; fileRef = 02ED50DA29A66007008341CD /* languages.json */; }; + 02F175312A4DA95B0019CD70 /* MainScreenAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F175302A4DA95B0019CD70 /* MainScreenAnalytics.swift */; }; 071009C928D1DB3F00344290 /* ScreenAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 071009C828D1DB3F00344290 /* ScreenAssembly.swift */; }; 0727876D28D23312002E9142 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727876C28D23312002E9142 /* Environment.swift */; }; 0727878E28D347C7002E9142 /* MainScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727878D28D347C7002E9142 /* MainScreenView.swift */; }; @@ -68,12 +70,14 @@ 025DE1A328DB4DAE0053E0F4 /* Profile.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Profile.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 025EF2F7297177F300B838AB /* NewEdX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NewEdX.entitlements; sourceTree = ""; }; 027DB32F28D8A063002B6862 /* Dashboard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Dashboard.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0298DF2F2A4EF7230023A257 /* AnalyticsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsManager.swift; sourceTree = ""; }; 02B6B3C428E1E61400232911 /* CourseDetails.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CourseDetails.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 02ED50CA29A64AAA008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02ED50D529A6554E008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = uk; path = "uk.lproj/сountries.json"; sourceTree = ""; }; 02ED50D729A65554008341CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = Base; path = "Base.lproj/сountries.json"; sourceTree = ""; }; 02ED50D929A66007008341CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = Base; path = Base.lproj/languages.json; sourceTree = ""; }; 02ED50DB29A6600B008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = uk; path = uk.lproj/languages.json; sourceTree = ""; }; + 02F175302A4DA95B0019CD70 /* MainScreenAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenAnalytics.swift; sourceTree = ""; }; 071009C828D1DB3F00344290 /* ScreenAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenAssembly.swift; sourceTree = ""; }; 0727876C28D23312002E9142 /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; 0727878D28D347C7002E9142 /* MainScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenView.swift; sourceTree = ""; }; @@ -162,6 +166,8 @@ 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */, 0770DE1628D080A1006D8A5D /* RouteController.swift */, 0770DE1F28D0858A006D8A5D /* Router.swift */, + 0298DF2F2A4EF7230023A257 /* AnalyticsManager.swift */, + 02F175302A4DA95B0019CD70 /* MainScreenAnalytics.swift */, 02512FF1299534300024D438 /* CoreDataHandler.swift */, 0770DE2628D09209006D8A5D /* SwiftUIHostController.swift */, 0727876C28D23312002E9142 /* Environment.swift */, @@ -221,6 +227,7 @@ 07D5DA2E28D075AA00752FD9 /* Frameworks */, 07D5DA2F28D075AA00752FD9 /* Resources */, 0770DE1528D07845006D8A5D /* Embed Frameworks */, + 02F175442A4E3B320019CD70 /* ShellScript */, ); buildRules = ( ); @@ -281,6 +288,25 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 02F175442A4E3B320019CD70 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 8; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", + "$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 1; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\n"; + }; 0770DE2328D08647006D8A5D /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -328,7 +354,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0298DF302A4EF7230023A257 /* AnalyticsManager.swift in Sources */, 07D5DA3528D075AA00752FD9 /* AppDelegate.swift in Sources */, + 02F175312A4DA95B0019CD70 /* MainScreenAnalytics.swift in Sources */, 02512FF2299534300024D438 /* CoreDataHandler.swift in Sources */, 0727878E28D347C7002E9142 /* MainScreenView.swift in Sources */, 0770DE5028D0A707006D8A5D /* NetworkAssembly.swift in Sources */, @@ -415,7 +443,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -599,7 +627,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -783,7 +811,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; diff --git a/NewEdX/AnalyticsManager.swift b/NewEdX/AnalyticsManager.swift new file mode 100644 index 000000000..c765cc648 --- /dev/null +++ b/NewEdX/AnalyticsManager.swift @@ -0,0 +1,369 @@ +// +// AnalyticsManager.swift +// NewEdX +// +// Created by  Stepanok Ivan on 27.06.2023. +// + +import Foundation +import Core +import FirebaseAnalytics +import Authorization +import Discovery +import Dashboard +import Profile +import Course +import Discussion + +class AnalyticsManager: AuthorizationAnalytics, + MainScreenAnalytics, + DiscoveryAnalytics, + DashboardAnalytics, + ProfileAnalytics, + CourseAnalytics, + DiscussionAnalytics { + + public func setUserID(_ id: String) { + Analytics.setUserID(id) + } + + public func userLogin(method: LoginMethod) { + logEvent(.userLogin, parameters: [Key.method: method.rawValue]) + } + + public func signUpClicked() { + logEvent(.signUpClicked) + } + + public func createAccountClicked() { + logEvent(.createAccountClicked) + } + + public func registrationSuccess() { + logEvent(.registrationSuccess) + } + + public func forgotPasswordClicked() { + logEvent(.forgotPasswordClicked) + } + + public func resetPasswordClicked(success: Bool) { + logEvent(.resetPasswordClicked, parameters: [Key.success: success]) + } + + // MARK: MainScreenAnalytics + + public func mainDiscoveryTabClicked() { + logEvent(.mainDiscoveryTabClicked) + } + + public func mainDashboardTabClicked() { + logEvent(.mainDashboardTabClicked) + } + + public func mainProgramsTabClicked() { + logEvent(.mainProgramsTabClicked) + } + + public func mainProfileTabClicked() { + logEvent(.mainProfileTabClicked) + } + + // MARK: Discovery + + public func discoverySearchBarClicked() { + logEvent(.discoverySearchBarClicked) + } + + public func discoveryCoursesSearch(label: String, coursesCount: Int) { + logEvent(.discoveryCoursesSearch, + parameters: [Key.label: label, + Key.coursesCount: coursesCount]) + } + + public func discoveryCourseClicked(courseID: String, courseName: String) { + let parameters = [ + Key.courseID: courseID, + Key.courseName: courseName + ] + logEvent(.discoveryCourseClicked, parameters: parameters) + } + + // MARK: Dashboard + + public func dashboardCourseClicked(courseID: String, courseName: String) { + let parameters = [ + Key.courseID: courseID, + Key.courseName: courseName + ] + logEvent(.dashboardCourseClicked, parameters: parameters) + } + + // MARK: Profile + + public func profileEditClicked() { + logEvent(.profileEditClicked) + } + + public func profileEditDoneClicked() { + logEvent(.profileEditDoneClicked) + } + + public func profileDeleteAccountClicked() { + logEvent(.profileDeleteAccountClicked) + } + + public func profileVideoSettingsClicked() { + logEvent(.profileVideoSettingsClicked) + } + + public func privacyPolicyClicked() { + logEvent(.privacyPolicyClicked) + } + + public func cookiePolicyClicked() { + logEvent(.cookiePolicyClicked) + } + + public func emailSupportClicked() { + logEvent(.emailSupportClicked) + } + + public func userLogout(force: Bool) { + logEvent(.userLogout, parameters: [Key.force: force]) + } + + // MARK: Course + + public func courseEnrollClicked(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.courseEnrollClicked, parameters: parameters) + } + + public func courseEnrollSuccess(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.courseEnrollSuccess, parameters: parameters) + } + + public func viewCourseClicked(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.viewCourseClicked, parameters: parameters) + } + + public func resumeCourseTapped(courseId: String, courseName: String, blockId: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName, + Key.blockID: blockId + ] + logEvent(.resumeCourseTapped, parameters: parameters) + } + + public func sequentialClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName, + Key.blockID: blockId, + Key.blockName: blockName + ] + logEvent(.sequentialClicked, parameters: parameters) + } + + public func verticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName, + Key.blockID: blockId, + Key.blockName: blockName + ] + logEvent(.verticalClicked, parameters: parameters) + } + + public func nextBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName, + Key.blockID: blockId, + Key.blockName: blockName + ] + logEvent(.nextBlockClicked, parameters: parameters) + } + + public func prevBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName, + Key.blockID: blockId, + Key.blockName: blockName + ] + logEvent(.prevBlockClicked, parameters: parameters) + } + + public func finishVerticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName, + Key.blockID: blockId, + Key.blockName: blockName + ] + logEvent(.finishVerticalClicked, parameters: parameters) + } + + public func finishVerticalNextSectionClicked( + courseId: String, + courseName: String, + blockId: String, + blockName: String + ) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName, + Key.blockID: blockId, + Key.blockName: blockName + ] + logEvent(.finishVerticalNextSectionClicked, parameters: parameters) + } + + public func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.finishVerticalBackToOutlineClicked, parameters: parameters) + } + + public func courseOutlineCourseTabClicked(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.courseOutlineCourseTabClicked, parameters: parameters) + } + + public func courseOutlineVideosTabClicked(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.courseOutlineVideosTabClicked, parameters: parameters) + } + + public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.courseOutlineDiscussionTabClicked, parameters: parameters) + } + + public func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.courseOutlineHandoutsTabClicked, parameters: parameters) + } + + // MARK: Discussion + public func discussionAllPostsClicked(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.discussionAllPostsClicked, parameters: parameters) + } + + public func discussionFollowingClicked(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.discussionFollowingClicked, parameters: parameters) + } + + public func discussionTopicClicked(courseId: String, courseName: String, topicId: String, topicName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName, + Key.topicID: topicId, + Key.topicName: topicName + ] + logEvent(.discussionTopicClicked, parameters: parameters) + } + + private func logEvent(_ event: Event, parameters: [String: Any]? = nil) { + Analytics.logEvent(event.rawValue, parameters: parameters) + } +} + +struct Key { + static let courseID = "course_id" + static let courseName = "course_name" + static let topicID = "topic_id" + static let topicName = "topic_name" + static let blockID = "block_id" + static let blockName = "block_name" + static let method = "method" + static let label = "label" + static let coursesCount = "courses_count" + static let force = "force" + static let success = "success" +} + +enum Event: String { + case userLogin = "User_Login" + case signUpClicked = "Sign_up_Clicked" + case createAccountClicked = "Create_Account_Clicked" + case registrationSuccess = "Registration_Success" + case userLogout = "User_Logout" + case forgotPasswordClicked = "Forgot_password_Clicked" + case resetPasswordClicked = "Reset_password_Clicked" + + case mainDiscoveryTabClicked = "Main_Discovery_tab_Clicked" + case mainDashboardTabClicked = "Main_Dashboard_tab_Clicked" + case mainProgramsTabClicked = "Main_Programs_tab_Clicked" + case mainProfileTabClicked = "Main_Profile_tab_Clicked" + + case discoverySearchBarClicked = "Discovery_Search_Bar_Clicked" + case discoveryCoursesSearch = "Discovery_Courses_Search" + case discoveryCourseClicked = "Discovery_Course_Clicked" + + case dashboardCourseClicked = "Dashboard_Course_Clicked" + + case profileEditClicked = "Profile_Edit_Clicked" + case profileEditDoneClicked = "Profile_Edit_Done_Clicked" + case profileDeleteAccountClicked = "Profile_Delete_Account_Clicked" + case profileVideoSettingsClicked = "Profile_Video_settings_Clicked" + case privacyPolicyClicked = "Privacy_Policy_Clicked" + case cookiePolicyClicked = "Cookie_Policy_Clicked" + case emailSupportClicked = "Email_Support_Clicked" + + case courseEnrollClicked = "Course_Enroll_Clicked" + case courseEnrollSuccess = "Course_Enroll_Success" + case viewCourseClicked = "View_Course_Clicked" + case resumeCourseTapped = "Resume_Course_Tapped" + case sequentialClicked = "Sequential_Clicked" + case verticalClicked = "Vertical_Clicked" + case nextBlockClicked = "Next_Block_Clicked" + case prevBlockClicked = "Prev_Block_Clicked" + case finishVerticalClicked = "Finish_Vertical_Clicked" + case finishVerticalNextSectionClicked = "Finish_Vertical_Next_section_Clicked" + case finishVerticalBackToOutlineClicked = "Finish_Vertical_Back_to_outline_Clicked" + case courseOutlineCourseTabClicked = "Course_Outline_Course_tab_Clicked" + case courseOutlineVideosTabClicked = "Course_Outline_Videos_tab_Clicked" + case courseOutlineDiscussionTabClicked = "Course_Outline_Discussion_tab_Clicked" + case courseOutlineHandoutsTabClicked = "Course_Outline_Handouts_tab_Clicked" + + case discussionAllPostsClicked = "Discussion_All_Posts_Clicked" + case discussionFollowingClicked = "Discussion_Following_Clicked" + case discussionTopicClicked = "Discussion_Topic_Clicked" +} diff --git a/NewEdX/AppDelegate.swift b/NewEdX/AppDelegate.swift index a52d40c6e..7e8cd9ad1 100644 --- a/NewEdX/AppDelegate.swift +++ b/NewEdX/AppDelegate.swift @@ -8,6 +8,10 @@ import UIKit import Core import Swinject +import FirebaseCore +import FirebaseAnalytics +import FirebaseCrashlytics +import Profile @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -29,6 +33,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + if BuildConfiguration.shared.firebaseOptions.apiKey != "" { + FirebaseApp.configure(options: BuildConfiguration.shared.firebaseOptions) + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true) + } + initDI() Theme.Fonts.registerFonts() @@ -76,6 +85,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { guard Date().timeIntervalSince1970 - lastForceLogoutTime > 5 else { return } + let analytics = Container.shared.resolve(AnalyticsManager.self) + analytics?.userLogout(force: true) + lastForceLogoutTime = Date().timeIntervalSince1970 Container.shared.resolve(AppStorage.self)?.clear() diff --git a/NewEdX/DI/AppAssembly.swift b/NewEdX/DI/AppAssembly.swift index 16cb8a055..111f05ab1 100644 --- a/NewEdX/DI/AppAssembly.swift +++ b/NewEdX/DI/AppAssembly.swift @@ -34,6 +34,38 @@ class AppAssembly: Assembly { Router(navigationController: r.resolve(UINavigationController.self)!, container: container) } + container.register(AnalyticsManager.self) { _ in + AnalyticsManager() + } + + container.register(AuthorizationAnalytics.self) { r in + r.resolve(AnalyticsManager.self)! + }.inObjectScope(.container) + + container.register(MainScreenAnalytics.self) { r in + r.resolve(AnalyticsManager.self)! + }.inObjectScope(.container) + + container.register(DiscoveryAnalytics.self) { r in + r.resolve(AnalyticsManager.self)! + }.inObjectScope(.container) + + container.register(DashboardAnalytics.self) { r in + r.resolve(AnalyticsManager.self)! + }.inObjectScope(.container) + + container.register(ProfileAnalytics.self) { r in + r.resolve(AnalyticsManager.self)! + }.inObjectScope(.container) + + container.register(CourseAnalytics.self) { r in + r.resolve(AnalyticsManager.self)! + }.inObjectScope(.container) + + container.register(DiscussionAnalytics.self) { r in + r.resolve(AnalyticsManager.self)! + }.inObjectScope(.container) + container.register(ConnectivityProtocol.self) { _ in Connectivity() } diff --git a/NewEdX/DI/ScreenAssembly.swift b/NewEdX/DI/ScreenAssembly.swift index e9fbf6b00..be302d701 100644 --- a/NewEdX/DI/ScreenAssembly.swift +++ b/NewEdX/DI/ScreenAssembly.swift @@ -47,6 +47,7 @@ class ScreenAssembly: Assembly { SignInViewModel( interactor: r.resolve(AuthInteractorProtocol.self)!, router: r.resolve(AuthorizationRouter.self)!, + analytics: r.resolve(AuthorizationAnalytics.self)!, validator: r.resolve(Validator.self)! ) } @@ -54,6 +55,7 @@ class ScreenAssembly: Assembly { SignUpViewModel( interactor: r.resolve(AuthInteractorProtocol.self)!, router: r.resolve(AuthorizationRouter.self)!, + analytics: r.resolve(AuthorizationAnalytics.self)!, config: r.resolve(Config.self)!, cssInjector: r.resolve(CSSInjector.self)!, validator: r.resolve(Validator.self)! @@ -63,6 +65,7 @@ class ScreenAssembly: Assembly { ResetPasswordViewModel( interactor: r.resolve(AuthInteractorProtocol.self)!, router: r.resolve(AuthorizationRouter.self)!, + analytics: r.resolve(AuthorizationAnalytics.self)!, validator: r.resolve(Validator.self)! ) } @@ -88,7 +91,8 @@ class ScreenAssembly: Assembly { container.register(DiscoveryViewModel.self) { r in DiscoveryViewModel( interactor: r.resolve(DiscoveryInteractorProtocol.self)!, - connectivity: r.resolve(ConnectivityProtocol.self)! + connectivity: r.resolve(ConnectivityProtocol.self)!, + analytics: r.resolve(DiscoveryAnalytics.self)! ) } @@ -97,6 +101,7 @@ class ScreenAssembly: Assembly { interactor: r.resolve(DiscoveryInteractorProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, router: r.resolve(DiscoveryRouter.self)!, + analytics: r.resolve(DiscoveryAnalytics.self)!, debounce: .searchDebounce ) } @@ -122,7 +127,8 @@ class ScreenAssembly: Assembly { container.register(DashboardViewModel.self) { r in DashboardViewModel( interactor: r.resolve(DashboardInteractorProtocol.self)!, - connectivity: r.resolve(ConnectivityProtocol.self)! + connectivity: r.resolve(ConnectivityProtocol.self)!, + analytics: r.resolve(DashboardAnalytics.self)! ) } @@ -145,6 +151,7 @@ class ScreenAssembly: Assembly { ProfileViewModel( interactor: r.resolve(ProfileInteractor.self)!, router: r.resolve(ProfileRouter.self)!, + analytics: r.resolve(ProfileAnalytics.self)!, config: r.resolve(Config.self)!, connectivity: r.resolve(ConnectivityProtocol.self)! ) @@ -153,7 +160,9 @@ class ScreenAssembly: Assembly { EditProfileViewModel( userModel: userModel, interactor: r.resolve(ProfileInteractor.self)!, - router: r.resolve(ProfileRouter.self)! + router: r.resolve(ProfileRouter.self)!, + analytics: r.resolve(ProfileAnalytics.self)! + ) } @@ -194,6 +203,7 @@ class ScreenAssembly: Assembly { CourseDetailsViewModel( interactor: r.resolve(CourseInteractorProtocol.self)!, router: r.resolve(CourseRouter.self)!, + analytics: r.resolve(CourseAnalytics.self)!, config: r.resolve(Config.self)!, cssInjector: r.resolve(CSSInjector.self)!, connectivity: r.resolve(ConnectivityProtocol.self)! @@ -208,6 +218,7 @@ class ScreenAssembly: Assembly { interactor: r.resolve(CourseInteractorProtocol.self)!, authInteractor: r.resolve(AuthInteractorProtocol.self)!, router: r.resolve(CourseRouter.self)!, + analytics: r.resolve(CourseAnalytics.self)!, config: r.resolve(Config.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, manager: r.resolve(DownloadManagerProtocol.self)!, @@ -226,23 +237,26 @@ class ScreenAssembly: Assembly { sequentialIndex: sequentialIndex, manager: r.resolve(DownloadManagerProtocol.self)!, router: r.resolve(CourseRouter.self)!, + analytics: r.resolve(CourseAnalytics.self)!, connectivity: r.resolve(ConnectivityProtocol.self)! ) } container.register( CourseUnitViewModel.self - ) { r, blockId, courseId, id, chapters, chapterIndex, sequentialIndex, verticalIndex in + ) { r, blockId, courseId, id, courseName, chapters, chapterIndex, sequentialIndex, verticalIndex in CourseUnitViewModel( lessonID: blockId, courseID: courseId, id: id, + courseName: courseName, chapters: chapters, chapterIndex: chapterIndex, sequentialIndex: sequentialIndex, verticalIndex: verticalIndex, interactor: r.resolve(CourseInteractorProtocol.self)!, router: r.resolve(CourseRouter.self)!, + analytics: r.resolve(CourseAnalytics.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, manager: r.resolve(DownloadManagerProtocol.self)! ) @@ -309,10 +323,12 @@ class ScreenAssembly: Assembly { ) } - container.register(DiscussionTopicsViewModel.self) { r in + container.register(DiscussionTopicsViewModel.self) { r, title in DiscussionTopicsViewModel( + title: title, interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, + analytics: r.resolve(DiscussionAnalytics.self)!, config: r.resolve(Config.self)! ) } diff --git a/NewEdX/Environment.swift b/NewEdX/Environment.swift index ac6f5d2ab..8e020c28c 100644 --- a/NewEdX/Environment.swift +++ b/NewEdX/Environment.swift @@ -6,6 +6,8 @@ // import Foundation +import Core +import FirebaseCore enum `Environment`: String { case debugDev = "DebugDev" @@ -45,6 +47,41 @@ class BuildConfiguration { } } + var firebaseOptions: FirebaseOptions { + switch environment { + case .debugDev, .releaseDev: + let firebaseOptions = FirebaseOptions(googleAppID: "", + gcmSenderID: "") + firebaseOptions.apiKey = "" + firebaseOptions.projectID = "" + firebaseOptions.bundleID = "" + firebaseOptions.clientID = "" + firebaseOptions.storageBucket = "" + + return firebaseOptions + case .debugStage, .releaseStage: + let firebaseOptions = FirebaseOptions(googleAppID: "", + gcmSenderID: "") + firebaseOptions.apiKey = "" + firebaseOptions.projectID = "" + firebaseOptions.bundleID = "" + firebaseOptions.clientID = "" + firebaseOptions.storageBucket = "" + + return firebaseOptions + case .debugProd, .releaseProd: + let firebaseOptions = FirebaseOptions(googleAppID: "", + gcmSenderID: "") + firebaseOptions.apiKey = "" + firebaseOptions.projectID = "" + firebaseOptions.bundleID = "" + firebaseOptions.clientID = "" + firebaseOptions.storageBucket = "" + + return firebaseOptions + } + } + init() { let currentConfiguration = Bundle.main.object(forInfoDictionaryKey: "Configuration") as! String environment = Environment(rawValue: currentConfiguration)! diff --git a/NewEdX/Info.plist b/NewEdX/Info.plist index b66a28b6c..66662cd72 100644 --- a/NewEdX/Info.plist +++ b/NewEdX/Info.plist @@ -4,11 +4,15 @@ Configuration $(CONFIGURATION) + FirebaseAppDelegateProxyEnabled + ITSAppUsesNonExemptEncryption UIAppFonts UIViewControllerBasedStatusBarAppearance + FirebaseAutomaticScreenReportingEnabled + diff --git a/NewEdX/MainScreenAnalytics.swift b/NewEdX/MainScreenAnalytics.swift new file mode 100644 index 000000000..8c554dffb --- /dev/null +++ b/NewEdX/MainScreenAnalytics.swift @@ -0,0 +1,16 @@ +// +// MainScreenAnalytics.swift +// NewEdX +// +// Created by  Stepanok Ivan on 29.06.2023. +// + +import Foundation + +//sourcery: AutoMockable +public protocol MainScreenAnalytics { + func mainDiscoveryTabClicked() + func mainDashboardTabClicked() + func mainProgramsTabClicked() + func mainProfileTabClicked() +} diff --git a/NewEdX/RouteController.swift b/NewEdX/RouteController.swift index 87e6c4204..8c2497e34 100644 --- a/NewEdX/RouteController.swift +++ b/NewEdX/RouteController.swift @@ -19,10 +19,15 @@ class RouteController: UIViewController { diContainer.resolve(AppStorage.self)! }() + private lazy var analytics: AuthorizationAnalytics = { + diContainer.resolve(AuthorizationAnalytics.self)! + }() + override func viewDidLoad() { super.viewDidLoad() - if appStorage.accessToken != nil && appStorage.user != nil { + if let user = appStorage.user, appStorage.accessToken != nil { + analytics.setUserID("\(user.id)") DispatchQueue.main.async { self.showMainScreen() } diff --git a/NewEdX/Router.swift b/NewEdX/Router.swift index 5a9acd4f3..c9f5acb6a 100644 --- a/NewEdX/Router.swift +++ b/NewEdX/Router.swift @@ -160,6 +160,8 @@ public class Router: AuthorizationRouter, public func showCourseVerticalView( id: String, + courseID: String, + courseName: String, title: String, chapters: [CourseChapter], chapterIndex: Int, @@ -172,7 +174,7 @@ public class Router: AuthorizationRouter, sequentialIndex )! - let view = CourseVerticalView(title: title, id: id, viewModel: viewModel) + let view = CourseVerticalView(title: title, courseName: courseName, courseID: courseID, id: id, viewModel: viewModel) let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -221,6 +223,7 @@ public class Router: AuthorizationRouter, } public func showCourseUnit( + courseName: String, id: String, blockId: String, courseID: String, @@ -235,6 +238,7 @@ public class Router: AuthorizationRouter, arguments: blockId, courseID, id, + courseName, chapters, chapterIndex, sequentialIndex, @@ -247,6 +251,7 @@ public class Router: AuthorizationRouter, public func replaceCourseUnit( id: String, + courseName: String, blockId: String, courseID: String, sectionName: String, @@ -265,6 +270,8 @@ public class Router: AuthorizationRouter, let viewVertical = CourseVerticalView( title: chapters[chapterIndex].childs[sequentialIndex].displayName, + courseName: courseName, + courseID: courseID, id: id, viewModel: vmVertical ) @@ -277,6 +284,7 @@ public class Router: AuthorizationRouter, arguments: blockId, courseID, id, + courseName, chapters, chapterIndex, sequentialIndex, diff --git a/NewEdX/View/MainScreenView.swift b/NewEdX/View/MainScreenView.swift index a3014b2f2..639efcbf4 100644 --- a/NewEdX/View/MainScreenView.swift +++ b/NewEdX/View/MainScreenView.swift @@ -11,6 +11,7 @@ import Core import Swinject import Dashboard import Profile +import SwiftUIIntrospect struct MainScreenView: View { @@ -23,6 +24,8 @@ struct MainScreenView: View { case profile } + let analytics = Container.shared.resolve(MainScreenAnalytics.self)! + init() { UITabBar.appearance().isTranslucent = false UITabBar.appearance().barTintColor = CoreAssets.textInputUnfocusedBackground.color @@ -41,8 +44,8 @@ struct MainScreenView: View { Text(CoreLocalization.Mainscreen.discovery) } .tag(MainTab.discovery) - .navigationBarHidden(true) - + .hideNavigationBar() + VStack { DashboardView( viewModel: Container.shared.resolve(DashboardViewModel.self)!, @@ -54,10 +57,7 @@ struct MainScreenView: View { Text(CoreLocalization.Mainscreen.dashboard) } .tag(MainTab.dashboard) - .navigationBarHidden(true) - .introspectViewController { vc in - vc.navigationController?.setNavigationBarHidden(true, animated: false) - } + .hideNavigationBar() VStack { Text(CoreLocalization.Mainscreen.inDeveloping) @@ -67,8 +67,8 @@ struct MainScreenView: View { Text(CoreLocalization.Mainscreen.programs) } .tag(MainTab.programs) - .navigationBarHidden(true) - + .hideNavigationBar() + VStack { ProfileView( viewModel: Container.shared.resolve(ProfileViewModel.self)! @@ -79,11 +79,20 @@ struct MainScreenView: View { Text(CoreLocalization.Mainscreen.profile) } .tag(MainTab.profile) - .navigationBarHidden(true) - .introspectViewController { vc in - vc.navigationController?.setNavigationBarHidden(true, animated: false) - } - } .navigationBarHidden(true) + .hideNavigationBar() + } + .onChange(of: selection, perform: { selection in + switch selection { + case .discovery: + analytics.mainDiscoveryTabClicked() + case .dashboard: + analytics.mainDashboardTabClicked() + case .programs: + analytics.mainProgramsTabClicked() + case .profile: + analytics.mainProfileTabClicked() + } + }) } struct MainScreenView_Previews: PreviewProvider { diff --git a/Podfile b/Podfile index 9166c04f7..1e80d0312 100644 --- a/Podfile +++ b/Podfile @@ -16,12 +16,16 @@ abstract_target "App" do target "Core" do project './Core/Core.xcodeproj' workspace './Core/Core.xcodeproj' + #Firebase + pod 'FirebaseAnalytics', '~> 10.11' + pod 'FirebaseCrashlytics', '~> 10.11' #Networking pod 'Alamofire', '~> 5.7' #Keychain pod 'KeychainSwift', '~> 20.0' #SwiftUI backward UIKit access - pod 'Introspect', '~> 0.6' + #pod 'Introspect', '~> 0.6' + pod 'SwiftUIIntrospect', '~> 0.8' pod 'Kingfisher', '~> 7.8' pod 'Swinject', '2.8.3' end diff --git a/Podfile.lock b/Podfile.lock index 17bcef55a..2d715ae54 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,36 +1,149 @@ PODS: - Alamofire (5.7.1) - - Introspect (0.6.2) + - FirebaseAnalytics (10.11.0): + - FirebaseAnalytics/AdIdSupport (= 10.11.0) + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseAnalytics/AdIdSupport (10.11.0): + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleAppMeasurement (= 10.11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseCore (10.11.0): + - FirebaseCoreInternal (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/Logger (~> 7.8) + - FirebaseCoreExtension (10.11.0): + - FirebaseCore (~> 10.0) + - FirebaseCoreInternal (10.11.0): + - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseCrashlytics (10.11.0): + - FirebaseCore (~> 10.5) + - FirebaseInstallations (~> 10.0) + - FirebaseSessions (~> 10.5) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.8) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesObjC (~> 2.1) + - FirebaseInstallations (10.11.0): + - FirebaseCore (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/UserDefaults (~> 7.8) + - PromisesObjC (~> 2.1) + - FirebaseSessions (10.11.0): + - FirebaseCore (~> 10.5) + - FirebaseCoreExtension (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.10) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesSwift (~> 2.1) + - GoogleAppMeasurement (10.11.0): + - GoogleAppMeasurement/AdIdSupport (= 10.11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (10.11.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (10.11.0): + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleDataTransport (9.2.3): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.11.1): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.11.1): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.11.1): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (7.11.1): + - GoogleUtilities/Logger + - GoogleUtilities/Network (7.11.1): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.11.1)" + - GoogleUtilities/Reachability (7.11.1): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.11.1): + - GoogleUtilities/Logger - KeychainSwift (20.0.0) - Kingfisher (7.8.1) + - nanopb (2.30909.0): + - nanopb/decode (= 2.30909.0) + - nanopb/encode (= 2.30909.0) + - nanopb/decode (2.30909.0) + - nanopb/encode (2.30909.0) + - PromisesObjC (2.2.0) + - PromisesSwift (2.2.0): + - PromisesObjC (= 2.2.0) - Sourcery (1.8.0): - Sourcery/CLI-Only (= 1.8.0) - Sourcery/CLI-Only (1.8.0) - SwiftGen (6.6.2) - - SwiftLint (0.52.2) + - SwiftLint (0.52.3) + - SwiftUIIntrospect (0.8.0) - SwiftyMocky (4.2.0): - Sourcery (= 1.8.0) - Swinject (2.8.3) DEPENDENCIES: - Alamofire (~> 5.7) - - Introspect (~> 0.6) + - FirebaseAnalytics (~> 10.11) + - FirebaseCrashlytics (~> 10.11) - KeychainSwift (~> 20.0) - Kingfisher (~> 7.8) - SwiftGen (~> 6.6) - SwiftLint (~> 0.5) + - SwiftUIIntrospect (~> 0.8) - SwiftyMocky (from `https://github.com/MakeAWishFoundation/SwiftyMocky.git`, tag `4.2.0`) - Swinject (= 2.8.3) SPEC REPOS: trunk: - Alamofire - - Introspect + - FirebaseAnalytics + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseCrashlytics + - FirebaseInstallations + - FirebaseSessions + - GoogleAppMeasurement + - GoogleDataTransport + - GoogleUtilities - KeychainSwift - Kingfisher + - nanopb + - PromisesObjC + - PromisesSwift - Sourcery - SwiftGen - SwiftLint + - SwiftUIIntrospect - Swinject EXTERNAL SOURCES: @@ -45,15 +158,28 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: Alamofire: 0123a34370cb170936ae79a8df46cc62b2edeb88 - Introspect: f80afac3cf8ff466700413368a5a2339144f71ce + FirebaseAnalytics: 6c6bf99e8854475bf1fa342028841be8ecd236da + FirebaseCore: 62fd4d549f5e3f3bd52b7998721c5fa0557fb355 + FirebaseCoreExtension: cacdad57fdb60e0b86dcbcac058ec78237946759 + FirebaseCoreInternal: 9e46c82a14a3b3a25be4e1e151ce6d21536b89c0 + FirebaseCrashlytics: 5927efd92f7fb052b0ab1e673d2f0d97274cd442 + FirebaseInstallations: 2a2c6859354cbec0a228a863d4daf6de7c74ced4 + FirebaseSessions: a62ba5c45284adb7714f4126cfbdb32b17c260bd + GoogleAppMeasurement: d3dabccdb336fc0ae44b633c8abaa26559893cd9 + GoogleDataTransport: f0308f5905a745f94fb91fea9c6cbaf3831cb1bd + GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749 KeychainSwift: 0ce6a4d13f7228054d1a71bb1b500448fb2ab837 Kingfisher: 63f677311d36a3473f6b978584f8a3845d023dc5 + nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef + PromisesSwift: cf9eb58666a43bbe007302226e510b16c1e10959 Sourcery: 6f5fe49b82b7e02e8c65560cbd52e1be67a1af2e SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c - SwiftLint: 1ac76dac888ca05cb0cf24d0c85887ec1209961d + SwiftLint: 76ec9c62ad369cff2937474cb34c9af3fa270b7b + SwiftUIIntrospect: cde309fef1f6690dd7585100453f1985f3b91c77 SwiftyMocky: c5e96e4ff76ec6dbf5a5941aeb039b5a546954a0 Swinject: 893c9a543000ac2f10ee4cbaf0933c6992c935d5 -PODFILE CHECKSUM: 20ef878e00f4ddcb660df8166a4c969e80e80901 +PODFILE CHECKSUM: e535ea6bf82dfd5be3ef0ea79b6907f520e57993 COCOAPODS: 1.12.0 diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index 135f89a25..87c456633 100644 --- a/Profile/Profile.xcodeproj/project.pbxproj +++ b/Profile/Profile.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 02A9A91D2978194A00B55797 /* ProfileViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A91C2978194A00B55797 /* ProfileViewModelTests.swift */; }; 02A9A91E2978194A00B55797 /* Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 020F834A28DB4CCD0062FA70 /* Profile.framework */; platformFilter = ios; }; 02A9A92B29781A6300B55797 /* ProfileMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A92A29781A6300B55797 /* ProfileMock.generated.swift */; }; + 02F175352A4DAD030019CD70 /* ProfileAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F175342A4DAD030019CD70 /* ProfileAnalytics.swift */; }; 02F3BFE7292539850051930C /* ProfileRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFE6292539850051930C /* ProfileRouter.swift */; }; 0796C8C929B7905300444B05 /* ProfileBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0796C8C829B7905300444B05 /* ProfileBottomSheet.swift */; }; 25B36FF48C1307888A3890DA /* Pods_App_Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEA369C38362C1A91A012F70 /* Pods_App_Profile.framework */; }; @@ -70,6 +71,7 @@ 02A9A91C2978194A00B55797 /* ProfileViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModelTests.swift; sourceTree = ""; }; 02A9A92A29781A6300B55797 /* ProfileMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileMock.generated.swift; sourceTree = ""; }; 02ED50CE29A64BAD008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + 02F175342A4DAD030019CD70 /* ProfileAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileAnalytics.swift; sourceTree = ""; }; 02F3BFE6292539850051930C /* ProfileRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRouter.swift; sourceTree = ""; }; 0796C8C829B7905300444B05 /* ProfileBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileBottomSheet.swift; sourceTree = ""; }; 0E5054C44435557666B6D885 /* Pods-App-Profile.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.debugstage.xcconfig"; sourceTree = ""; }; @@ -175,6 +177,7 @@ 0203DC3C29AE79EB0017BD05 /* EditProfile */, 0262149029AE5793008BD75A /* DeleteAccount */, 02F3BFE6292539850051930C /* ProfileRouter.swift */, + 02F175342A4DAD030019CD70 /* ProfileAnalytics.swift */, ); path = Presentation; sourceTree = ""; @@ -547,6 +550,7 @@ 0259104829C3A5F0004B5A55 /* VideoQualityView.swift in Sources */, 021D924628DC634300ACC565 /* ProfileView.swift in Sources */, 02F3BFE7292539850051930C /* ProfileRouter.swift in Sources */, + 02F175352A4DAD030019CD70 /* ProfileAnalytics.swift in Sources */, 0262149429AE57B1008BD75A /* DeleteAccountViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift index 0005797aa..93ba44d04 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift @@ -47,6 +47,7 @@ public struct EditProfileView: View { rightButtonAction: { if viewModel.isChanged { Task { + viewModel.analytics.profileEditDoneClicked() await viewModel.saveProfileUpdates() } } @@ -139,6 +140,7 @@ public struct EditProfileView: View { }) Button(ProfileLocalization.Edit.deleteAccount, action: { + viewModel.analytics.profileDeleteAccountClicked() viewModel.router.showDeleteProfileView() }) .font(Theme.Fonts.labelLarge) @@ -245,7 +247,8 @@ struct EditProfileView_Previews: PreviewProvider { viewModel: EditProfileViewModel( userModel: userModel, interactor: ProfileInteractor.mock, - router: ProfileRouterMock()), + router: ProfileRouterMock(), + analytics: ProfileAnalyticsMock()), avatar: nil, profileDidEdit: {_ in} ) diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift index e34fbef0c..ab76a9d76 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift @@ -77,11 +77,16 @@ public class EditProfileViewModel: ObservableObject { private let interactor: ProfileInteractorProtocol let router: ProfileRouter + let analytics: ProfileAnalytics - public init(userModel: UserProfile, interactor: ProfileInteractorProtocol, router: ProfileRouter) { + public init(userModel: UserProfile, + interactor: ProfileInteractorProtocol, + router: ProfileRouter, + analytics: ProfileAnalytics) { self.userModel = userModel self.interactor = interactor self.router = router + self.analytics = analytics self.spokenLanguages = interactor.getSpokenLanguages() self.countries = interactor.getCountries() generateYears() diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index 1d3f9baf5..ec3879bc5 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -29,6 +29,7 @@ public struct ProfileView: View { rightButtonType: .edit, rightButtonAction: { if let userModel = viewModel.userModel { + viewModel.analytics.profileEditClicked() viewModel.router.showEditProfile( userModel: userModel, avatar: viewModel.updatedAvatar, @@ -105,6 +106,7 @@ public struct ProfileView: View { VStack(alignment: .leading, spacing: 27) { HStack { Button(action: { + viewModel.analytics.profileVideoSettingsClicked() viewModel.router.showSettings() }, label: { Text(ProfileLocalization.settingsVideo) @@ -123,37 +125,54 @@ public struct ProfileView: View { .font(Theme.Fonts.labelLarge) VStack(alignment: .leading, spacing: 24) { if let support = viewModel.contactSupport() { - HStack { - Link(destination: support, label: { + Button(action: { + viewModel.analytics.emailSupportClicked() + UIApplication.shared.open(support) + }, label: { + HStack { Text(ProfileLocalization.contact) Spacer() Image(systemName: "chevron.right") - }) - } + } + }) + .buttonStyle(PlainButtonStyle()) + .foregroundColor(.primary) Rectangle() .frame(height: 1) .foregroundColor(CoreAssets.textSecondary.swiftUIColor) } + if let tos = viewModel.config.termsOfUse { - HStack { - Link(destination: tos, label: { + Button(action: { + viewModel.analytics.cookiePolicyClicked() + UIApplication.shared.open(tos) + }, label: { + HStack { Text(ProfileLocalization.terms) Spacer() Image(systemName: "chevron.right") - }) - } + } + }) + .buttonStyle(PlainButtonStyle()) + .foregroundColor(.primary) Rectangle() .frame(height: 1) .foregroundColor(CoreAssets.textSecondary.swiftUIColor) } + if let privacy = viewModel.config.privacyPolicy { - HStack { - Link(destination: privacy, label: { + Button(action: { + viewModel.analytics.privacyPolicyClicked() + UIApplication.shared.open(privacy) + }, label: { + HStack { Text(ProfileLocalization.privacy) Spacer() Image(systemName: "chevron.right") - }) - } + } + }) + .buttonStyle(PlainButtonStyle()) + .foregroundColor(.primary) } }.cardStyle( bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor, @@ -174,6 +193,7 @@ public struct ProfileView: View { }, okTapped: { Task { + viewModel.analytics.userLogout(force: false) await viewModel.logOut() } viewModel.router.dismiss(animated: true) @@ -234,6 +254,7 @@ struct ProfileView_Previews: PreviewProvider { let router = ProfileRouterMock() let vm = ProfileViewModel(interactor: ProfileInteractor.mock, router: router, + analytics: ProfileAnalyticsMock(), config: ConfigMock(), connectivity: Connectivity()) diff --git a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift index a8640cf51..8439adbc2 100644 --- a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift +++ b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift @@ -25,15 +25,18 @@ public class ProfileViewModel: ObservableObject { private let interactor: ProfileInteractorProtocol let router: ProfileRouter + let analytics: ProfileAnalytics let config: Config let connectivity: ConnectivityProtocol public init(interactor: ProfileInteractorProtocol, router: ProfileRouter, + analytics: ProfileAnalytics, config: Config, connectivity: ConnectivityProtocol) { self.interactor = interactor self.router = router + self.analytics = analytics self.config = config self.connectivity = connectivity } diff --git a/Profile/Profile/Presentation/ProfileAnalytics.swift b/Profile/Profile/Presentation/ProfileAnalytics.swift new file mode 100644 index 000000000..58cc4b9d9 --- /dev/null +++ b/Profile/Profile/Presentation/ProfileAnalytics.swift @@ -0,0 +1,33 @@ +// +// ProfileAnalytics.swift +// Profile +// +// Created by  Stepanok Ivan on 29.06.2023. +// + +import Foundation + +//sourcery: AutoMockable +public protocol ProfileAnalytics { + func profileEditClicked() + func profileEditDoneClicked() + func profileDeleteAccountClicked() + func profileVideoSettingsClicked() + func privacyPolicyClicked() + func cookiePolicyClicked() + func emailSupportClicked() + func userLogout(force: Bool) +} + +#if DEBUG +class ProfileAnalyticsMock: ProfileAnalytics { + public func profileEditClicked() {} + public func profileEditDoneClicked() {} + public func profileDeleteAccountClicked() {} + public func profileVideoSettingsClicked() {} + public func privacyPolicyClicked() {} + public func cookiePolicyClicked() {} + public func emailSupportClicked() {} + public func userLogout(force: Bool) {} +} +#endif diff --git a/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift index c5f1e63ee..47280627c 100644 --- a/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift @@ -18,6 +18,7 @@ final class EditProfileViewModelTests: XCTestCase { func testResizeVerticalImage() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userProfile = UserProfile( avatarUrl: "url", name: "Test", @@ -33,7 +34,12 @@ final class EditProfileViewModelTests: XCTestCase { Given(interactor, .getSpokenLanguages(willReturn: [])) Given(interactor, .getCountries(willReturn: [])) - let viewModel = EditProfileViewModel(userModel: userProfile, interactor: interactor, router: router) + let viewModel = EditProfileViewModel( + userModel: userProfile, + interactor: interactor, + router: router, + analytics: analytics + ) let imageVertical = UIGraphicsImageRenderer(size: CGSize(width: 600, height: 800)).image { rendererContext in UIColor.red.setFill() @@ -49,6 +55,7 @@ final class EditProfileViewModelTests: XCTestCase { func testResizeHorizontalImage() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userProfile = UserProfile( avatarUrl: "url", name: "Test", @@ -67,7 +74,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userProfile, interactor: interactor, - router: router + router: router, + analytics: analytics ) let imageHorizontal = UIGraphicsImageRenderer(size: CGSize(width: 800, height: 600)).image { rendererContext in @@ -84,6 +92,7 @@ final class EditProfileViewModelTests: XCTestCase { func testCheckChangesShortBiographyChanged() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userProfile = UserProfile( avatarUrl: "url", name: "Test", @@ -102,7 +111,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userProfile, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.profileChanges.shortBiography = "New bio" @@ -114,6 +124,7 @@ final class EditProfileViewModelTests: XCTestCase { func testCheckChangesSpokenLanguageChanged() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -132,7 +143,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.spokenLanguageConfiguration.text = "Changed" @@ -144,6 +156,7 @@ final class EditProfileViewModelTests: XCTestCase { func testCheckChangesBirthYearChanged() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -162,7 +175,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.yearsConfiguration.text = "Changed" @@ -174,6 +188,7 @@ final class EditProfileViewModelTests: XCTestCase { func testCheckChangesAvatarChanged() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -192,7 +207,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.profileChanges.isAvatarChanged = true @@ -204,6 +220,7 @@ final class EditProfileViewModelTests: XCTestCase { func testCheckChangesProfileTypeChanged() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -222,7 +239,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.profileChanges.profileType = .limited @@ -234,6 +252,7 @@ final class EditProfileViewModelTests: XCTestCase { func testCheckChangesCountryChanged() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -252,7 +271,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.countriesConfiguration.text = "Changed" @@ -264,6 +284,7 @@ final class EditProfileViewModelTests: XCTestCase { func testCheckProfileTypeNotYongUser() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -282,7 +303,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.profileChanges.profileType = viewModel.userModel.isFullProfile ? .full : .limited @@ -298,6 +320,7 @@ final class EditProfileViewModelTests: XCTestCase { func testCheckProfileTypeIsYongerUser() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -318,7 +341,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.yearsConfiguration.text = "\(yearOfBirth10Years - 1)" @@ -332,6 +356,7 @@ final class EditProfileViewModelTests: XCTestCase { func testCheckProfileTypeYearsConfigurationEmpty() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -350,7 +375,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.yearsConfiguration.text = "" @@ -365,6 +391,7 @@ final class EditProfileViewModelTests: XCTestCase { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -383,7 +410,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.yearsConfiguration.text = "" @@ -398,6 +426,7 @@ final class EditProfileViewModelTests: XCTestCase { func testSaveProfileUpdates() async { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -416,7 +445,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.countriesConfiguration.text = "USA" @@ -444,6 +474,7 @@ final class EditProfileViewModelTests: XCTestCase { func testDeleteAvatarSuccess() async { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -462,7 +493,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.profileChanges.isAvatarDeleted = true @@ -485,6 +517,7 @@ final class EditProfileViewModelTests: XCTestCase { func testSaveProfileUpdatesNoInternetError() async { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -503,7 +536,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.countriesConfiguration.text = "USA" @@ -539,6 +573,7 @@ final class EditProfileViewModelTests: XCTestCase { func testSaveProfileUpdatesUnknownError() async { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -557,7 +592,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.countriesConfiguration.text = "USA" @@ -591,6 +627,7 @@ final class EditProfileViewModelTests: XCTestCase { func testBackButtonTapped() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -609,7 +646,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.profileChanges.isAvatarChanged = true @@ -627,6 +665,7 @@ final class EditProfileViewModelTests: XCTestCase { func testGenerateFieldConfigurationsFullProfile() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -645,7 +684,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.loadLocationsAndSpokenLanguages() @@ -657,6 +697,7 @@ final class EditProfileViewModelTests: XCTestCase { func testGenerateFieldConfigurationsLimitedProfile() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -675,7 +716,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.loadLocationsAndSpokenLanguages() @@ -686,6 +728,7 @@ final class EditProfileViewModelTests: XCTestCase { func testLoadLocationsAndSpokenLanguages() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let userModel = UserProfile( avatarUrl: "url", name: "Test", @@ -709,7 +752,8 @@ final class EditProfileViewModelTests: XCTestCase { let viewModel = EditProfileViewModel( userModel: userModel, interactor: interactor, - router: router + router: router, + analytics: analytics ) viewModel.loadLocationsAndSpokenLanguages() diff --git a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift index 870229912..6c6502921 100644 --- a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift @@ -17,10 +17,12 @@ final class ProfileViewModelTests: XCTestCase { func testGetMyProfileSuccess() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), connectivity: connectivity) @@ -49,10 +51,12 @@ final class ProfileViewModelTests: XCTestCase { func testGetMyProfileOfflineSuccess() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), connectivity: connectivity) @@ -81,10 +85,12 @@ final class ProfileViewModelTests: XCTestCase { func testGetMyProfileNoInternetError() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), connectivity: connectivity) @@ -105,10 +111,12 @@ final class ProfileViewModelTests: XCTestCase { func testGetMyProfileNoCacheError() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), connectivity: connectivity) @@ -127,10 +135,12 @@ final class ProfileViewModelTests: XCTestCase { func testGetMyProfileUnknownError() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), connectivity: connectivity) @@ -149,10 +159,12 @@ final class ProfileViewModelTests: XCTestCase { func testLogOutSuccess() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), connectivity: connectivity) @@ -168,10 +180,12 @@ final class ProfileViewModelTests: XCTestCase { func testLogOutNoInternetError() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), connectivity: connectivity) @@ -189,10 +203,12 @@ final class ProfileViewModelTests: XCTestCase { func testLogOutUnknownError() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel(interactor: interactor, router: router, + analytics: analytics, config: ConfigMock(), connectivity: connectivity) diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index a13760a6f..8d71c7ff2 100644 --- a/Profile/ProfileTests/ProfileMock.generated.swift +++ b/Profile/ProfileTests/ProfileMock.generated.swift @@ -121,17 +121,20 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws { + open func registerUser(fields: [String: String]) throws -> User { addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void perform?(`fields`) + var __value: User do { - _ = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() as Void + __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() } catch MockError.notStubed { - // do nothing + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") } catch { throw error } + return __value } open func validateRegistrationFields(fields: [String: String]) throws -> [String: String] { @@ -233,6 +236,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -281,10 +287,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given } @@ -996,6 +1002,286 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } +// MARK: - ProfileAnalytics + +open class ProfileAnalyticsMock: ProfileAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func profileEditClicked() { + addInvocation(.m_profileEditClicked) + let perform = methodPerformValue(.m_profileEditClicked) as? () -> Void + perform?() + } + + open func profileEditDoneClicked() { + addInvocation(.m_profileEditDoneClicked) + let perform = methodPerformValue(.m_profileEditDoneClicked) as? () -> Void + perform?() + } + + open func profileDeleteAccountClicked() { + addInvocation(.m_profileDeleteAccountClicked) + let perform = methodPerformValue(.m_profileDeleteAccountClicked) as? () -> Void + perform?() + } + + open func profileVideoSettingsClicked() { + addInvocation(.m_profileVideoSettingsClicked) + let perform = methodPerformValue(.m_profileVideoSettingsClicked) as? () -> Void + perform?() + } + + open func privacyPolicyClicked() { + addInvocation(.m_privacyPolicyClicked) + let perform = methodPerformValue(.m_privacyPolicyClicked) as? () -> Void + perform?() + } + + open func cookiePolicyClicked() { + addInvocation(.m_cookiePolicyClicked) + let perform = methodPerformValue(.m_cookiePolicyClicked) as? () -> Void + perform?() + } + + open func emailSupportClicked() { + addInvocation(.m_emailSupportClicked) + let perform = methodPerformValue(.m_emailSupportClicked) as? () -> Void + perform?() + } + + open func userLogout(force: Bool) { + addInvocation(.m_userLogout__force_force(Parameter.value(`force`))) + let perform = methodPerformValue(.m_userLogout__force_force(Parameter.value(`force`))) as? (Bool) -> Void + perform?(`force`) + } + + + fileprivate enum MethodType { + case m_profileEditClicked + case m_profileEditDoneClicked + case m_profileDeleteAccountClicked + case m_profileVideoSettingsClicked + case m_privacyPolicyClicked + case m_cookiePolicyClicked + case m_emailSupportClicked + case m_userLogout__force_force(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_profileEditClicked, .m_profileEditClicked): return .match + + case (.m_profileEditDoneClicked, .m_profileEditDoneClicked): return .match + + case (.m_profileDeleteAccountClicked, .m_profileDeleteAccountClicked): return .match + + case (.m_profileVideoSettingsClicked, .m_profileVideoSettingsClicked): return .match + + case (.m_privacyPolicyClicked, .m_privacyPolicyClicked): return .match + + case (.m_cookiePolicyClicked, .m_cookiePolicyClicked): return .match + + case (.m_emailSupportClicked, .m_emailSupportClicked): return .match + + case (.m_userLogout__force_force(let lhsForce), .m_userLogout__force_force(let rhsForce)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .m_profileEditClicked: return 0 + case .m_profileEditDoneClicked: return 0 + case .m_profileDeleteAccountClicked: return 0 + case .m_profileVideoSettingsClicked: return 0 + case .m_privacyPolicyClicked: return 0 + case .m_cookiePolicyClicked: return 0 + case .m_emailSupportClicked: return 0 + case let .m_userLogout__force_force(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_profileEditClicked: return ".profileEditClicked()" + case .m_profileEditDoneClicked: return ".profileEditDoneClicked()" + case .m_profileDeleteAccountClicked: return ".profileDeleteAccountClicked()" + case .m_profileVideoSettingsClicked: return ".profileVideoSettingsClicked()" + case .m_privacyPolicyClicked: return ".privacyPolicyClicked()" + case .m_cookiePolicyClicked: return ".cookiePolicyClicked()" + case .m_emailSupportClicked: return ".emailSupportClicked()" + case .m_userLogout__force_force: return ".userLogout(force:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func profileEditClicked() -> Verify { return Verify(method: .m_profileEditClicked)} + public static func profileEditDoneClicked() -> Verify { return Verify(method: .m_profileEditDoneClicked)} + public static func profileDeleteAccountClicked() -> Verify { return Verify(method: .m_profileDeleteAccountClicked)} + public static func profileVideoSettingsClicked() -> Verify { return Verify(method: .m_profileVideoSettingsClicked)} + public static func privacyPolicyClicked() -> Verify { return Verify(method: .m_privacyPolicyClicked)} + public static func cookiePolicyClicked() -> Verify { return Verify(method: .m_cookiePolicyClicked)} + public static func emailSupportClicked() -> Verify { return Verify(method: .m_emailSupportClicked)} + public static func userLogout(force: Parameter) -> Verify { return Verify(method: .m_userLogout__force_force(`force`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func profileEditClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_profileEditClicked, performs: perform) + } + public static func profileEditDoneClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_profileEditDoneClicked, performs: perform) + } + public static func profileDeleteAccountClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_profileDeleteAccountClicked, performs: perform) + } + public static func profileVideoSettingsClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_profileVideoSettingsClicked, performs: perform) + } + public static func privacyPolicyClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_privacyPolicyClicked, performs: perform) + } + public static func cookiePolicyClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_cookiePolicyClicked, performs: perform) + } + public static func emailSupportClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_emailSupportClicked, performs: perform) + } + public static func userLogout(force: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_userLogout__force_force(`force`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + // MARK: - ProfileInteractorProtocol open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { From a2f83df7939c8b425ec5feda93c21f5839a3792f Mon Sep 17 00:00:00 2001 From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com> Date: Wed, 5 Jul 2023 16:28:57 +0300 Subject: [PATCH 26/26] Rename project to the OpenEdX (#46) * rename workspace * .gitignore update --------- Co-authored-by: Volodymyr Chekyrta --- .gitignore | 6 +- .../contents.xcworkspacedata | 2 +- .../Authorization.xcodeproj/project.pbxproj | 32 +- .../contents.xcworkspacedata | 2 +- Core/Core.xcodeproj/project.pbxproj | 32 +- Core/Core/Analytics/AnalyticsManager.swift | 294 ------------------ Core/Core/Configuration/Connectivity.swift | 2 +- .../contents.xcworkspacedata | 2 +- Course/Course.xcodeproj/project.pbxproj | 32 +- .../contents.xcworkspacedata | 2 +- Dashboard/Dashboard.xcodeproj/project.pbxproj | 32 +- .../contents.xcworkspacedata | 2 +- Discovery/Discovery.xcodeproj/project.pbxproj | 32 +- .../contents.xcworkspacedata | 5 +- .../Discussion.xcodeproj/project.pbxproj | 32 +- .../project.pbxproj | 118 +++---- .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../UserInterfaceState.xcuserstate | Bin .../xcschemes/OpenEdXDev.xcscheme | 18 +- .../xcschemes/OpenEdXProd.xcscheme | 18 +- .../xcschemes/OpenEdXStage.xcscheme | 18 +- .../contents.xcworkspacedata | 2 +- .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 5 + {NewEdX => OpenEdX}/AnalyticsManager.swift | 2 +- {NewEdX => OpenEdX}/AppDelegate.swift | 2 +- .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/app icon.jpg | Bin .../Assets.xcassets/Contents.json | 0 .../SplachBackground.colorset/Contents.json | 0 .../appLogo.imageset/Contents.json | 0 .../appLogo.imageset/Group 21.svg | 0 .../Base.lproj/LaunchScreen.storyboard | 0 {NewEdX => OpenEdX}/Base.lproj/languages.json | 0 .../Base.lproj/\321\201ountries.json" | 0 OpenEdX/Configuration.json | 26 ++ {NewEdX => OpenEdX}/CoreDataHandler.swift | 2 +- {NewEdX => OpenEdX}/DI/AppAssembly.swift | 2 +- {NewEdX => OpenEdX}/DI/NetworkAssembly.swift | 2 +- {NewEdX => OpenEdX}/DI/ScreenAssembly.swift | 2 +- {NewEdX => OpenEdX}/Environment.swift | 2 +- {NewEdX => OpenEdX}/Info.plist | 4 +- {NewEdX => OpenEdX}/MainScreenAnalytics.swift | 2 +- .../OpenEdX.entitlements | 0 {NewEdX => OpenEdX}/RouteController.swift | 2 +- {NewEdX => OpenEdX}/Router.swift | 2 +- .../SwiftUIHostController.swift | 2 +- {NewEdX => OpenEdX}/View/MainScreenView.swift | 2 +- .../en.lproj/Localizable.strings | 2 +- .../uk.lproj/LaunchScreen.strings | 0 .../uk.lproj/Localizable.strings | 2 +- {NewEdX => OpenEdX}/uk.lproj/languages.json | 0 .../uk.lproj/\321\201ountries.json" | 0 Podfile | 4 +- Podfile.lock | 4 +- .../contents.xcworkspacedata | 2 +- Profile/Profile.xcodeproj/project.pbxproj | 48 +-- README.md | 8 +- fastlane/Fastfile | 4 +- 61 files changed, 276 insertions(+), 540 deletions(-) delete mode 100644 Core/Core/Analytics/AnalyticsManager.swift rename {NewEdX.xcodeproj => OpenEdX.xcodeproj}/project.pbxproj (89%) rename {NewEdX.xcodeproj => OpenEdX.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename {NewEdX.xcodeproj => OpenEdX.xcodeproj}/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {NewEdX.xcodeproj => OpenEdX.xcodeproj}/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate (100%) rename NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXDev.xcscheme => OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXDev.xcscheme (92%) rename NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXProd.xcscheme => OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXProd.xcscheme (94%) rename NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXStage.xcscheme => OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXStage.xcscheme (92%) rename {NewEdX.xcworkspace => OpenEdX.xcworkspace}/contents.xcworkspacedata (94%) rename {NewEdX.xcworkspace => OpenEdX.xcworkspace}/xcshareddata/IDEWorkspaceChecks.plist (100%) create mode 100644 OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename {NewEdX => OpenEdX}/AnalyticsManager.swift (99%) rename {NewEdX => OpenEdX}/AppDelegate.swift (99%) rename {NewEdX => OpenEdX}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename {NewEdX => OpenEdX}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {NewEdX => OpenEdX}/Assets.xcassets/AppIcon.appiconset/app icon.jpg (100%) rename {NewEdX => OpenEdX}/Assets.xcassets/Contents.json (100%) rename {NewEdX => OpenEdX}/Assets.xcassets/SplachBackground.colorset/Contents.json (100%) rename {NewEdX => OpenEdX}/Assets.xcassets/appLogo.imageset/Contents.json (100%) rename {NewEdX => OpenEdX}/Assets.xcassets/appLogo.imageset/Group 21.svg (100%) rename {NewEdX => OpenEdX}/Base.lproj/LaunchScreen.storyboard (100%) rename {NewEdX => OpenEdX}/Base.lproj/languages.json (100%) rename "NewEdX/Base.lproj/\321\201ountries.json" => "OpenEdX/Base.lproj/\321\201ountries.json" (100%) create mode 100644 OpenEdX/Configuration.json rename {NewEdX => OpenEdX}/CoreDataHandler.swift (98%) rename {NewEdX => OpenEdX}/DI/AppAssembly.swift (99%) rename {NewEdX => OpenEdX}/DI/NetworkAssembly.swift (99%) rename {NewEdX => OpenEdX}/DI/ScreenAssembly.swift (99%) rename {NewEdX => OpenEdX}/Environment.swift (99%) rename {NewEdX => OpenEdX}/Info.plist (86%) rename {NewEdX => OpenEdX}/MainScreenAnalytics.swift (96%) rename NewEdX/NewEdX.entitlements => OpenEdX/OpenEdX.entitlements (100%) rename {NewEdX => OpenEdX}/RouteController.swift (99%) rename {NewEdX => OpenEdX}/Router.swift (99%) rename {NewEdX => OpenEdX}/SwiftUIHostController.swift (99%) rename {NewEdX => OpenEdX}/View/MainScreenView.swift (99%) rename {NewEdX => OpenEdX}/en.lproj/Localizable.strings (88%) rename {NewEdX => OpenEdX}/uk.lproj/LaunchScreen.strings (100%) rename {NewEdX => OpenEdX}/uk.lproj/Localizable.strings (88%) rename {NewEdX => OpenEdX}/uk.lproj/languages.json (100%) rename "NewEdX/uk.lproj/\321\201ountries.json" => "OpenEdX/uk.lproj/\321\201ountries.json" (100%) diff --git a/.gitignore b/.gitignore index 3e3e2a9d2..998e24696 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,9 @@ ## User settings xcuserdata/* -/NewEdX.xcodeproj/xcuserdata/ -/NewEdX.xcworkspace/xcuserdata/ -/NewEdX.xcworkspace/xcshareddata/swiftpm/Package.resolved +/OpenEdX.xcodeproj/xcuserdata/ +/OpenEdX.xcworkspace/xcuserdata/ +/OpenEdX.xcworkspace/xcshareddata/swiftpm/Package.resolved ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) *.xcscmblueprint diff --git a/Authorization/Authorization.xcodeproj.xcworkspace/contents.xcworkspacedata b/Authorization/Authorization.xcodeproj.xcworkspace/contents.xcworkspacedata index 080e833b6..7ae574247 100644 --- a/Authorization/Authorization.xcodeproj.xcworkspace/contents.xcworkspacedata +++ b/Authorization/Authorization.xcodeproj.xcworkspace/contents.xcworkspacedata @@ -8,7 +8,7 @@ location = "group:../Core/Core.xcodeproj"> + location = "group:../OpenEdX.xcodeproj"> diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index 6eed8e006..b4c539d33 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -590,7 +590,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Authorization; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Authorization; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -613,7 +613,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.AuthorizationTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -701,7 +701,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Authorization; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Authorization; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -723,7 +723,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.AuthorizationTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -741,7 +741,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.AuthorizationTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -759,7 +759,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.AuthorizationTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -777,7 +777,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.AuthorizationTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -795,7 +795,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.AuthorizationTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -813,7 +813,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.AuthorizationTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -831,7 +831,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.AuthorizationTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -925,7 +925,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Authorization; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Authorization; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1018,7 +1018,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Authorization; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Authorization; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1116,7 +1116,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Authorization; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Authorization; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1209,7 +1209,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Authorization; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Authorization; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1365,7 +1365,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Authorization; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Authorization; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1400,7 +1400,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Authorization; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Authorization; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/Core/Core.xcodeproj.xcworkspace/contents.xcworkspacedata b/Core/Core.xcodeproj.xcworkspace/contents.xcworkspacedata index f211704ff..0efc22dd7 100644 --- a/Core/Core.xcodeproj.xcworkspace/contents.xcworkspacedata +++ b/Core/Core.xcodeproj.xcworkspace/contents.xcworkspacedata @@ -5,7 +5,7 @@ location = "group:Core.xcodeproj"> + location = "group:../OpenEdX.xcodeproj"> diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 8a681437e..a23953164 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -958,7 +958,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Core; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -980,7 +980,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CoreTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1071,7 +1071,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Core; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1092,7 +1092,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CoreTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1112,7 +1112,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CoreTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1132,7 +1132,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CoreTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1152,7 +1152,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CoreTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1172,7 +1172,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CoreTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1192,7 +1192,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CoreTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1212,7 +1212,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CoreTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1309,7 +1309,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Core; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1402,7 +1402,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Core; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1500,7 +1500,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Core; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1593,7 +1593,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Core; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1749,7 +1749,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Core; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1784,7 +1784,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Core; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/Core/Core/Analytics/AnalyticsManager.swift b/Core/Core/Analytics/AnalyticsManager.swift deleted file mode 100644 index 9972ab43a..000000000 --- a/Core/Core/Analytics/AnalyticsManager.swift +++ /dev/null @@ -1,294 +0,0 @@ -// -// AnalyticsManager.swift -// NewEdX -// -// Created by  Stepanok Ivan on 27.06.2023. -// - -import Foundation -import FirebaseAnalytics -import Authorization -import Discovery -import Dashboard -import Profile -import Course -import Discussion - -public protocol BaseCourseAnalytics { - associatedtype Event: RawRepresentable where Event.RawValue == String - func logEvent(_ event: Event, parameters: [String: Any]?) -} - -public class AnalyticsManager: BaseCourseAnalytics, - AuthorizationAnalytics, - MainScreenAnalytics, - DiscoveryAnalytics, - DashboardAnalytics, - ProfileAnalytics, - CourseAnalytics, - DiscussionAnalytics { - - public enum Event: String { - case userLogin = "User_Login" - case signUpClicked = "Sign_up_Clicked" - case createAccountClicked = "Create_Account_Clicked" - case registrationSuccess = "Registration_Success" - case userLogout = "User_Logout" - case forgotPasswordClicked = "Forgot_password_Clicked" - case resetPasswordClicked = "Reset_password_Clicked" - - case mainDiscoveryTabClicked = "Main_Discovery_tab_Clicked" - case mainDashboardTabClicked = "Main_Dashboard_tab_Clicked" - case mainProgramsTabClicked = "Main_Programs_tab_Clicked" - case mainProfileTabClicked = "Main_Profile_tab_Clicked" - - case discoverySearchBarClicked = "Discovery_Search_Bar_Clicked" - case discoveryCoursesSearch = "Discovery_Courses_Search" - case discoveryCourseClicked = "Discovery_Course_Clicked" - - case dashboardCourseClicked = "Dashboard_Course_Clicked" - - case profileEditClicked = "Profile_Edit_Clicked" - case profileEditDoneClicked = "Profile_Edit_Done_Clicked" - case profileDeleteAccountClicked = "Profile_Delete_Account_Clicked" - case profileVideoSettingsClicked = "Profile_Video_settings_Clicked" - case privacyPolicyClicked = "Privacy_Policy_Clicked" - case cookiePolicyClicked = "Cookie_Policy_Clicked" - case emailSupportClicked = "Email_Support_Clicked" - - case courseEnrollClicked = "Course_Enroll_Clicked" - case courseEnrollSuccess = "Course_Enroll_Success" - case viewCourseClicked = "View_Course_Clicked" - case resumeCourseTapped = "Resume_Course_Tapped" - case sequentialClicked = "Sequential_Clicked" - case verticalClicked = "Vertical_Clicked" - case nextBlockClicked = "Next_Block_Clicked" - case prevBlockClicked = "Prev_Block_Clicked" - case finishVerticalClicked = "Finish_Vertical_Clicked" - case finishVerticalNextSectionClicked = "Finish_Vertical_Next_section_Clicked" - case finishVerticalBackToOutlineClicked = "Finish_Vertical_Back_to_outline_Clicked" - case courseOutlineCourseTabClicked = "Course_Outline_Course_tab_Clicked" - case courseOutlineVideosTabClicked = "Course_Outline_Videos_tab_Clicked" - case courseOutlineDiscussionTabClicked = "Course_Outline_Discussion_tab_Clicked" - case courseOutlineHandoutsTabClicked = "Course_Outline_Handouts_tab_Clicked" - - case discussionAllPostsClicked = "Discussion_All_Posts_Clicked" - case discussionFollowingClicked = "Discussion_Following_Clicked" - case discussionTopicClicked = "Discussion_Topic_Clicked" - } - - public func logEvent(_ event: Event, parameters: [String: Any]?) { - Analytics.setAnalyticsCollectionEnabled(true) - Analytics.logEvent(event.rawValue, parameters: parameters) - } - - public func userLogin(method: LoginMethod) { - logEvent(.userLogin, parameters: ["method": method.rawValue]) - } - - public func signUpClicked() { - logEvent(.signUpClicked, parameters: nil) - } - - public func createAccountClicked(provider: LoginProvider) { - logEvent(.createAccountClicked, - parameters: ["provider": provider.rawValue]) - } - - public func registrationSuccess(provider: LoginProvider) { - logEvent(.registrationSuccess, - parameters: ["provider": provider.rawValue]) - } - - public func forgotPasswordClicked() { - logEvent(.forgotPasswordClicked, parameters: nil) - } - - public func resetPasswordClicked(success: Bool) { - logEvent(.resetPasswordClicked, parameters: ["success": success]) - } - - // MARK: MainScreenAnalytics - - public func mainDiscoveryTabClicked() { - logEvent(.mainDiscoveryTabClicked, parameters: nil) - } - - public func mainDashboardTabClicked() { - logEvent(.mainDashboardTabClicked, parameters: nil) - } - - public func mainProgramsTabClicked() { - logEvent(.mainProgramsTabClicked, parameters: nil) - } - - public func mainProfileTabClicked() { - logEvent(.mainProfileTabClicked, parameters: nil) - } - - // MARK: Discovery - - public func discoverySearchBarClicked() { - logEvent(.discoverySearchBarClicked, parameters: nil) - } - - public func discoveryCoursesSearch(label: String, coursesCount: Int) { - logEvent(.discoveryCoursesSearch, - parameters: ["label": label, - "courses_count": coursesCount]) - } - - public func discoveryCourseClicked(courseID: String, courseName: String) { - logEvent(.discoveryCourseClicked, parameters: ["course_id": courseID, - "course_name": courseName]) - } - - // MARK: Dashboard - - public func dashboardCourseClicked(courseID: String, courseName: String) { - logEvent(.dashboardCourseClicked, parameters: ["course_id": courseID, - "course_name": courseName]) - } - - // MARK: Profile - - public func profileEditClicked() { - logEvent(.profileEditClicked, parameters: nil) - } - - public func profileEditDoneClicked() { - logEvent(.profileEditDoneClicked, parameters: nil) - } - - public func profileDeleteAccountClicked() { - logEvent(.profileDeleteAccountClicked, parameters: nil) - } - - public func profileVideoSettingsClicked() { - logEvent(.profileVideoSettingsClicked, parameters: nil) - } - - public func privacyPolicyClicked() { - logEvent(.privacyPolicyClicked, parameters: nil) - } - - public func cookiePolicyClicked() { - logEvent(.cookiePolicyClicked, parameters: nil) - } - - public func emailSupportClicked() { - logEvent(.emailSupportClicked, parameters: nil) - } - - public func userLogout(force: Bool) { - logEvent(.userLogout, parameters: ["force": force]) - } - - // MARK: Course - - public func courseEnrollClicked(courseId: String, courseName: String) { - logEvent(.courseEnrollClicked, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - public func courseEnrollSuccess(courseId: String, courseName: String) { - logEvent(.courseEnrollSuccess, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - public func viewCourseClicked(courseId: String, courseName: String) { - logEvent(.viewCourseClicked, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - public func resumeCourseTapped(courseId: String, courseName: String, blockId: String) { - logEvent(.resumeCourseTapped, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - public func sequentialClicked(courseId: String, courseName: String, blockId: String, blockName: String) { - logEvent(.sequentialClicked, parameters: ["course_id": courseId, - "course_name": courseName, - "block_id": blockId, - "block_name": blockName]) - } - - public func verticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) { - logEvent(.verticalClicked, parameters: ["course_id": courseId, - "course_name": courseName, - "block_id": blockId, - "block_name": blockName]) - } - - public func nextBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) { - logEvent(.nextBlockClicked, parameters: ["course_id": courseId, - "course_name": courseName, - "block_id": blockId, - "block_name": blockName]) - } - - public func prevBlockClicked(courseId: String, courseName: String, blockId: String, blockName: String) { - logEvent(.prevBlockClicked, parameters: ["course_id": courseId, - "course_name": courseName, - "block_id": blockId, - "block_name": blockName]) - } - - public func finishVerticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) { - logEvent(.finishVerticalClicked, parameters: ["course_id": courseId, - "course_name": courseName, - "block_id": blockId, - "block_name": blockName]) - } - - public func finishVerticalNextSectionClicked(courseId: String, courseName: String, blockId: String, blockName: String) { - logEvent(.finishVerticalNextSectionClicked, parameters: ["course_id": courseId, - "course_name": courseName, - "block_id": blockId, - "block_name": blockName]) - } - - public func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) { - logEvent(.finishVerticalBackToOutlineClicked, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - public func courseOutlineCourseTabClicked(courseId: String, courseName: String) { - logEvent(.courseOutlineCourseTabClicked, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - public func courseOutlineVideosTabClicked(courseId: String, courseName: String) { - logEvent(.courseOutlineVideosTabClicked, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) { - logEvent(.courseOutlineDiscussionTabClicked, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - public func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) { - logEvent(.courseOutlineHandoutsTabClicked, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - // MARK: Discussion - - public func discussionAllPostsClicked(courseId: String, courseName: String) { - logEvent(.discussionAllPostsClicked, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - public func discussionFollowingClicked(courseId: String, courseName: String) { - logEvent(.discussionFollowingClicked, parameters: ["course_id": courseId, - "course_name": courseName]) - } - - public func discussionTopicClicked(courseId: String, courseName: String, topicId: String, topicName: String) { - logEvent(.discussionAllPostsClicked, parameters: ["course_id": courseId, - "course_name": courseName, - "topic_id": topicId, - "topic_name": topicName]) - } -} diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 7a04f494a..b825a9ddb 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -1,6 +1,6 @@ // // Connectivity.swift -// NewEdX +// OpenEdX // // Created by  Stepanok Ivan on 15.12.2022. // diff --git a/Course/Course.xcodeproj.xcworkspace/contents.xcworkspacedata b/Course/Course.xcodeproj.xcworkspace/contents.xcworkspacedata index b74ad64b7..6e6981f01 100644 --- a/Course/Course.xcodeproj.xcworkspace/contents.xcworkspacedata +++ b/Course/Course.xcodeproj.xcworkspace/contents.xcworkspacedata @@ -17,7 +17,7 @@ location = "group:../Discovery/Discovery.xcodeproj"> + location = "group:../OpenEdX.xcodeproj"> diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index 5af6ffb85..9baaaedb5 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -755,7 +755,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.CourseTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -776,7 +776,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.CourseTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -797,7 +797,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.CourseTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -818,7 +818,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.CourseTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -839,7 +839,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.CourseTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -860,7 +860,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.CourseTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1014,7 +1014,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CourseDetails; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseDetails; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1049,7 +1049,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CourseDetails; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseDetails; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1147,7 +1147,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CourseDetails; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseDetails; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1246,7 +1246,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CourseDetails; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseDetails; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1339,7 +1339,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CourseDetails; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseDetails; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1431,7 +1431,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CourseDetails; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseDetails; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1529,7 +1529,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CourseDetails; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseDetails; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1553,7 +1553,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.CourseTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1643,7 +1643,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.CourseDetails; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseDetails; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1666,7 +1666,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.CourseTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; diff --git a/Dashboard/Dashboard.xcodeproj.xcworkspace/contents.xcworkspacedata b/Dashboard/Dashboard.xcodeproj.xcworkspace/contents.xcworkspacedata index 33772a869..fe487fea7 100644 --- a/Dashboard/Dashboard.xcodeproj.xcworkspace/contents.xcworkspacedata +++ b/Dashboard/Dashboard.xcodeproj.xcworkspace/contents.xcworkspacedata @@ -14,7 +14,7 @@ location = "group:../Discovery/Discovery.xcodeproj"> + location = "group:../OpenEdX.xcodeproj"> diff --git a/Dashboard/Dashboard.xcodeproj/project.pbxproj b/Dashboard/Dashboard.xcodeproj/project.pbxproj index b5103d7da..4aee39a87 100644 --- a/Dashboard/Dashboard.xcodeproj/project.pbxproj +++ b/Dashboard/Dashboard.xcodeproj/project.pbxproj @@ -491,7 +491,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DashboardTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -512,7 +512,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DashboardTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -533,7 +533,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DashboardTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -554,7 +554,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DashboardTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -575,7 +575,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DashboardTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -596,7 +596,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DashboardTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -692,7 +692,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Dashboard; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Dashboard; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -716,7 +716,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DashboardTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -806,7 +806,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Dashboard; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Dashboard; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -829,7 +829,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DashboardTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -983,7 +983,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Dashboard; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Dashboard; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1018,7 +1018,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Dashboard; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Dashboard; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1116,7 +1116,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Dashboard; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Dashboard; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1209,7 +1209,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Dashboard; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Dashboard; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1307,7 +1307,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Dashboard; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Dashboard; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1400,7 +1400,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Dashboard; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Dashboard; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/Discovery/Discovery.xcodeproj.xcworkspace/contents.xcworkspacedata b/Discovery/Discovery.xcodeproj.xcworkspace/contents.xcworkspacedata index 9ccd451c4..21947269f 100644 --- a/Discovery/Discovery.xcodeproj.xcworkspace/contents.xcworkspacedata +++ b/Discovery/Discovery.xcodeproj.xcworkspace/contents.xcworkspacedata @@ -11,7 +11,7 @@ location = "group:Discovery.xcodeproj"> + location = "group:../OpenEdX.xcodeproj"> diff --git a/Discovery/Discovery.xcodeproj/project.pbxproj b/Discovery/Discovery.xcodeproj/project.pbxproj index b6213f2ff..59b0d96ae 100644 --- a/Discovery/Discovery.xcodeproj/project.pbxproj +++ b/Discovery/Discovery.xcodeproj/project.pbxproj @@ -517,7 +517,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscoveryUnitTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -538,7 +538,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscoveryUnitTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -559,7 +559,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscoveryUnitTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -580,7 +580,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscoveryUnitTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -601,7 +601,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscoveryUnitTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -622,7 +622,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscoveryUnitTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -718,7 +718,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discovery; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discovery; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -742,7 +742,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscoveryUnitTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -832,7 +832,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discovery; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discovery; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -855,7 +855,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscoveryUnitTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1009,7 +1009,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discovery; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discovery; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1044,7 +1044,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discovery; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discovery; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1142,7 +1142,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discovery; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discovery; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1241,7 +1241,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discovery; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discovery; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1334,7 +1334,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discovery; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discovery; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1426,7 +1426,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discovery; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discovery; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/Discussion/Discussion.xcodeproj.xcworkspace/contents.xcworkspacedata b/Discussion/Discussion.xcodeproj.xcworkspace/contents.xcworkspacedata index 698b13400..85b36c90c 100644 --- a/Discussion/Discussion.xcodeproj.xcworkspace/contents.xcworkspacedata +++ b/Discussion/Discussion.xcodeproj.xcworkspace/contents.xcworkspacedata @@ -19,9 +19,6 @@ - - @@ -29,6 +26,6 @@ location = "group:../Pods/Pods.xcodeproj"> + location = "group:../OpenEdX.xcodeproj"> diff --git a/Discussion/Discussion.xcodeproj/project.pbxproj b/Discussion/Discussion.xcodeproj/project.pbxproj index 8f4b4c86b..cfd6abaff 100644 --- a/Discussion/Discussion.xcodeproj/project.pbxproj +++ b/Discussion/Discussion.xcodeproj/project.pbxproj @@ -889,7 +889,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discussion; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discussion; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -923,7 +923,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discussion; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discussion; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1020,7 +1020,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discussion; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discussion; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1118,7 +1118,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discussion; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discussion; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1210,7 +1210,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discussion; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discussion; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1301,7 +1301,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discussion; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discussion; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1324,7 +1324,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscussionTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1345,7 +1345,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscussionTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1366,7 +1366,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscussionTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1387,7 +1387,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscussionTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1408,7 +1408,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscussionTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1429,7 +1429,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscussionTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1524,7 +1524,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discussion; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discussion; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1548,7 +1548,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscussionTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1637,7 +1637,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Discussion; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Discussion; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1660,7 +1660,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.DiscussionTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; diff --git a/NewEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj similarity index 89% rename from NewEdX.xcodeproj/project.pbxproj rename to OpenEdX.xcodeproj/project.pbxproj index 90dfa72b7..35a452168 100644 --- a/NewEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -38,7 +38,7 @@ 07D5DA3528D075AA00752FD9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */; }; 07D5DA3E28D075AB00752FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */; }; 07D5DA4128D075AB00752FD9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3F28D075AB00752FD9 /* LaunchScreen.storyboard */; }; - F9D8E3EA58261028C3968180 /* Pods_App_NewEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16B61E47E3D4524ED0463D63 /* Pods_App_NewEdX.framework */; }; + 95C140F3BDF778364986E83B /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F138C15C3A2515F8F94DAA8B /* Pods_App_OpenEdX.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -68,7 +68,7 @@ 02512FF1299534300024D438 /* CoreDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandler.swift; sourceTree = ""; }; 025C77A028E463E900B3DFA3 /* CourseOutline.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CourseOutline.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 025DE1A328DB4DAE0053E0F4 /* Profile.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Profile.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 025EF2F7297177F300B838AB /* NewEdX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NewEdX.entitlements; sourceTree = ""; }; + 025EF2F7297177F300B838AB /* OpenEdX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenEdX.entitlements; sourceTree = ""; }; 027DB32F28D8A063002B6862 /* Dashboard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Dashboard.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0298DF2F2A4EF7230023A257 /* AnalyticsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsManager.swift; sourceTree = ""; }; 02B6B3C428E1E61400232911 /* CourseDetails.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CourseDetails.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -91,17 +91,17 @@ 0770DE4F28D0A707006D8A5D /* NetworkAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAssembly.swift; sourceTree = ""; }; 0770DE6528D0BCC7006D8A5D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 07A7D78E28F5C9060000BE81 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 07D5DA3128D075AA00752FD9 /* NewEdX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NewEdX.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 07D5DA3128D075AA00752FD9 /* OpenEdX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenEdX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 07D5DA4028D075AB00752FD9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 16B61E47E3D4524ED0463D63 /* Pods_App_NewEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_NewEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 31F7D13F89ABEA0C978F4988 /* Pods-App-NewEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.debugstage.xcconfig"; sourceTree = ""; }; - 92E99692D520163574381814 /* Pods-App-NewEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.releasestage.xcconfig"; sourceTree = ""; }; - B65009A9300CA1C0AA1ADA13 /* Pods-App-NewEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.debugprod.xcconfig"; sourceTree = ""; }; - B7A07A23F1B105D41A41426C /* Pods-App-NewEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.debugdev.xcconfig"; sourceTree = ""; }; - DAFCB10EBCC10B9CE95D5140 /* Pods-App-NewEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.releasedev.xcconfig"; sourceTree = ""; }; - E8680E764DC8A25C24EA74D5 /* Pods-App-NewEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-NewEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-NewEdX/Pods-App-NewEdX.releaseprod.xcconfig"; sourceTree = ""; }; + 1499CCAD7A0D8A3E6AF39794 /* Pods-App-OpenEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugprod.xcconfig"; sourceTree = ""; }; + 6F54C19C823A769E18923FA8 /* Pods-App-OpenEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugstage.xcconfig"; sourceTree = ""; }; + 8284179FC05AEE2591573E20 /* Pods-App-OpenEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugdev.xcconfig"; sourceTree = ""; }; + A24D6A8E1BC4DF46AD68904C /* Pods-App-OpenEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releaseprod.xcconfig"; sourceTree = ""; }; + A89AD827F52CF6A6B903606E /* Pods-App-OpenEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasestage.xcconfig"; sourceTree = ""; }; + BB08ACD2CCA33D6DDDDD31B4 /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; + F138C15C3A2515F8F94DAA8B /* Pods_App_OpenEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_OpenEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -116,7 +116,7 @@ 0218196428F734FA00202564 /* Discussion.framework in Frameworks */, 0219C67728F4347600D64452 /* Course.framework in Frameworks */, 027DB33028D8A063002B6862 /* Dashboard.framework in Frameworks */, - F9D8E3EA58261028C3968180 /* Pods_App_NewEdX.framework in Frameworks */, + 95C140F3BDF778364986E83B /* Pods_App_OpenEdX.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -144,7 +144,7 @@ 07D5DA2828D075AA00752FD9 = { isa = PBXGroup; children = ( - 07D5DA3328D075AA00752FD9 /* NewEdX */, + 07D5DA3328D075AA00752FD9 /* OpenEdX */, 07D5DA3228D075AA00752FD9 /* Products */, 55A895025FB07897BA68E063 /* Pods */, 4E6FB43543890E90BB88D64D /* Frameworks */, @@ -154,15 +154,15 @@ 07D5DA3228D075AA00752FD9 /* Products */ = { isa = PBXGroup; children = ( - 07D5DA3128D075AA00752FD9 /* NewEdX.app */, + 07D5DA3128D075AA00752FD9 /* OpenEdX.app */, ); name = Products; sourceTree = ""; }; - 07D5DA3328D075AA00752FD9 /* NewEdX */ = { + 07D5DA3328D075AA00752FD9 /* OpenEdX */ = { isa = PBXGroup; children = ( - 025EF2F7297177F300B838AB /* NewEdX.entitlements */, + 025EF2F7297177F300B838AB /* OpenEdX.entitlements */, 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */, 0770DE1628D080A1006D8A5D /* RouteController.swift */, 0770DE1F28D0858A006D8A5D /* Router.swift */, @@ -180,7 +180,7 @@ 02ED50D629A6554E008341CD /* сountries.json */, 0770DE6628D0BCC7006D8A5D /* Localizable.strings */, ); - path = NewEdX; + path = OpenEdX; sourceTree = ""; }; 4E6FB43543890E90BB88D64D /* Frameworks */ = { @@ -196,7 +196,7 @@ 072787B028D34D83002E9142 /* Discovery.framework */, 0770DE4A28D0A462006D8A5D /* Authorization.framework */, 0770DE1228D07845006D8A5D /* Core.framework */, - 16B61E47E3D4524ED0463D63 /* Pods_App_NewEdX.framework */, + F138C15C3A2515F8F94DAA8B /* Pods_App_OpenEdX.framework */, ); name = Frameworks; sourceTree = ""; @@ -204,12 +204,12 @@ 55A895025FB07897BA68E063 /* Pods */ = { isa = PBXGroup; children = ( - B65009A9300CA1C0AA1ADA13 /* Pods-App-NewEdX.debugprod.xcconfig */, - 31F7D13F89ABEA0C978F4988 /* Pods-App-NewEdX.debugstage.xcconfig */, - B7A07A23F1B105D41A41426C /* Pods-App-NewEdX.debugdev.xcconfig */, - E8680E764DC8A25C24EA74D5 /* Pods-App-NewEdX.releaseprod.xcconfig */, - 92E99692D520163574381814 /* Pods-App-NewEdX.releasestage.xcconfig */, - DAFCB10EBCC10B9CE95D5140 /* Pods-App-NewEdX.releasedev.xcconfig */, + 1499CCAD7A0D8A3E6AF39794 /* Pods-App-OpenEdX.debugprod.xcconfig */, + 6F54C19C823A769E18923FA8 /* Pods-App-OpenEdX.debugstage.xcconfig */, + 8284179FC05AEE2591573E20 /* Pods-App-OpenEdX.debugdev.xcconfig */, + A24D6A8E1BC4DF46AD68904C /* Pods-App-OpenEdX.releaseprod.xcconfig */, + A89AD827F52CF6A6B903606E /* Pods-App-OpenEdX.releasestage.xcconfig */, + BB08ACD2CCA33D6DDDDD31B4 /* Pods-App-OpenEdX.releasedev.xcconfig */, ); path = Pods; sourceTree = ""; @@ -217,11 +217,11 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 07D5DA3028D075AA00752FD9 /* NewEdX */ = { + 07D5DA3028D075AA00752FD9 /* OpenEdX */ = { isa = PBXNativeTarget; - buildConfigurationList = 07D5DA4528D075AB00752FD9 /* Build configuration list for PBXNativeTarget "NewEdX" */; + buildConfigurationList = 07D5DA4528D075AB00752FD9 /* Build configuration list for PBXNativeTarget "OpenEdX" */; buildPhases = ( - 46EB6B53644A79B05619EEAD /* [CP] Check Pods Manifest.lock */, + 3165870BC90D2FA438CFF0A9 /* [CP] Check Pods Manifest.lock */, 0770DE2328D08647006D8A5D /* SwiftLint */, 07D5DA2D28D075AA00752FD9 /* Sources */, 07D5DA2E28D075AA00752FD9 /* Frameworks */, @@ -233,9 +233,9 @@ ); dependencies = ( ); - name = NewEdX; - productName = NewEdX; - productReference = 07D5DA3128D075AA00752FD9 /* NewEdX.app */; + name = OpenEdX; + productName = OpenEdX; + productReference = 07D5DA3128D075AA00752FD9 /* OpenEdX.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -253,7 +253,7 @@ }; }; }; - buildConfigurationList = 07D5DA2C28D075AA00752FD9 /* Build configuration list for PBXProject "NewEdX" */; + buildConfigurationList = 07D5DA2C28D075AA00752FD9 /* Build configuration list for PBXProject "OpenEdX" */; compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; @@ -267,7 +267,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 07D5DA3028D075AA00752FD9 /* NewEdX */, + 07D5DA3028D075AA00752FD9 /* OpenEdX */, ); }; /* End PBXProject section */ @@ -325,7 +325,7 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; - 46EB6B53644A79B05619EEAD /* [CP] Check Pods Manifest.lock */ = { + 3165870BC90D2FA438CFF0A9 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -340,7 +340,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-App-NewEdX-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-App-OpenEdX-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -472,16 +472,16 @@ }; 02DD1C9629E80CC200F35DCE /* DebugStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 31F7D13F89ABEA0C978F4988 /* Pods-App-NewEdX.debugstage.xcconfig */; + baseConfigurationReference = 6F54C19C823A769E18923FA8 /* Pods-App-OpenEdX.debugstage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = NewEdX/NewEdX.entitlements; + CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = NewEdX/Info.plist; + INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -494,7 +494,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.NewEdX.stage; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.stage; PRODUCT_NAME = "$(TARGET_NAME) Stage"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -561,16 +561,16 @@ }; 02DD1C9829E80CCB00F35DCE /* ReleaseStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 92E99692D520163574381814 /* Pods-App-NewEdX.releasestage.xcconfig */; + baseConfigurationReference = A89AD827F52CF6A6B903606E /* Pods-App-OpenEdX.releasestage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = NewEdX/NewEdX.entitlements; + CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = NewEdX/Info.plist; + INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -583,7 +583,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.NewEdX.stage; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.stage; PRODUCT_NAME = "$(TARGET_NAME) Stage"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -656,16 +656,16 @@ }; 0727875928D231FD002E9142 /* DebugDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B7A07A23F1B105D41A41426C /* Pods-App-NewEdX.debugdev.xcconfig */; + baseConfigurationReference = 8284179FC05AEE2591573E20 /* Pods-App-OpenEdX.debugdev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = NewEdX/NewEdX.entitlements; + CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = NewEdX/Info.plist; + INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -678,7 +678,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.NewEdX.dev; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.dev; PRODUCT_NAME = "$(TARGET_NAME) Dev"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -745,16 +745,16 @@ }; 0727875B28D23204002E9142 /* ReleaseDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DAFCB10EBCC10B9CE95D5140 /* Pods-App-NewEdX.releasedev.xcconfig */; + baseConfigurationReference = BB08ACD2CCA33D6DDDDD31B4 /* Pods-App-OpenEdX.releasedev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = NewEdX/NewEdX.entitlements; + CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = NewEdX/Info.plist; + INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -767,7 +767,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.NewEdX.dev; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.dev; PRODUCT_NAME = "$(TARGET_NAME) Dev"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -894,16 +894,16 @@ }; 07D5DA4628D075AB00752FD9 /* DebugProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B65009A9300CA1C0AA1ADA13 /* Pods-App-NewEdX.debugprod.xcconfig */; + baseConfigurationReference = 1499CCAD7A0D8A3E6AF39794 /* Pods-App-OpenEdX.debugprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = NewEdX/NewEdX.entitlements; + CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = NewEdX/Info.plist; + INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -916,7 +916,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.NewEdX; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -929,16 +929,16 @@ }; 07D5DA4728D075AB00752FD9 /* ReleaseProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E8680E764DC8A25C24EA74D5 /* Pods-App-NewEdX.releaseprod.xcconfig */; + baseConfigurationReference = A24D6A8E1BC4DF46AD68904C /* Pods-App-OpenEdX.releaseprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = NewEdX/NewEdX.entitlements; + CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = NewEdX/Info.plist; + INFOPLIST_FILE = OpenEdX/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -951,7 +951,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.NewEdX; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -965,7 +965,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 07D5DA2C28D075AA00752FD9 /* Build configuration list for PBXProject "NewEdX" */ = { + 07D5DA2C28D075AA00752FD9 /* Build configuration list for PBXProject "OpenEdX" */ = { isa = XCConfigurationList; buildConfigurations = ( 07D5DA4328D075AB00752FD9 /* DebugProd */, @@ -978,7 +978,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = ReleaseProd; }; - 07D5DA4528D075AB00752FD9 /* Build configuration list for PBXNativeTarget "NewEdX" */ = { + 07D5DA4528D075AB00752FD9 /* Build configuration list for PBXNativeTarget "OpenEdX" */ = { isa = XCConfigurationList; buildConfigurations = ( 07D5DA4628D075AB00752FD9 /* DebugProd */, diff --git a/NewEdX.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/OpenEdX.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from NewEdX.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to OpenEdX.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/NewEdX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/OpenEdX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from NewEdX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to OpenEdX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/NewEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate b/OpenEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate similarity index 100% rename from NewEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate rename to OpenEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate diff --git a/NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXDev.xcscheme b/OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXDev.xcscheme similarity index 92% rename from NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXDev.xcscheme rename to OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXDev.xcscheme index 598dba3ba..3a38de2f5 100644 --- a/NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXDev.xcscheme +++ b/OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXDev.xcscheme @@ -15,9 +15,9 @@ + BuildableName = "OpenEdX.app" + BlueprintName = "OpenEdX" + ReferencedContainer = "container:OpenEdX.xcodeproj"> @@ -114,9 +114,9 @@ + BuildableName = "OpenEdX.app" + BlueprintName = "OpenEdX" + ReferencedContainer = "container:OpenEdX.xcodeproj"> + BuildableName = "OpenEdX.app" + BlueprintName = "OpenEdX" + ReferencedContainer = "container:OpenEdX.xcodeproj"> diff --git a/NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXProd.xcscheme b/OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXProd.xcscheme similarity index 94% rename from NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXProd.xcscheme rename to OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXProd.xcscheme index ec6caa524..7e7b99171 100644 --- a/NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXProd.xcscheme +++ b/OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXProd.xcscheme @@ -15,9 +15,9 @@ + BuildableName = "OpenEdX.app" + BlueprintName = "OpenEdX" + ReferencedContainer = "container:OpenEdX.xcodeproj"> @@ -150,9 +150,9 @@ + BuildableName = "OpenEdX.app" + BlueprintName = "OpenEdX" + ReferencedContainer = "container:OpenEdX.xcodeproj"> + BuildableName = "OpenEdX.app" + BlueprintName = "OpenEdX" + ReferencedContainer = "container:OpenEdX.xcodeproj"> diff --git a/NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXStage.xcscheme b/OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXStage.xcscheme similarity index 92% rename from NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXStage.xcscheme rename to OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXStage.xcscheme index 9d9ab0527..8e378a196 100644 --- a/NewEdX.xcodeproj/xcshareddata/xcschemes/NewEdXStage.xcscheme +++ b/OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXStage.xcscheme @@ -15,9 +15,9 @@ + BuildableName = "OpenEdX.app" + BlueprintName = "OpenEdX" + ReferencedContainer = "container:OpenEdX.xcodeproj"> @@ -114,9 +114,9 @@ + BuildableName = "OpenEdX.app" + BlueprintName = "OpenEdX" + ReferencedContainer = "container:OpenEdX.xcodeproj"> + BuildableName = "OpenEdX.app" + BlueprintName = "OpenEdX" + ReferencedContainer = "container:OpenEdX.xcodeproj"> diff --git a/NewEdX.xcworkspace/contents.xcworkspacedata b/OpenEdX.xcworkspace/contents.xcworkspacedata similarity index 94% rename from NewEdX.xcworkspace/contents.xcworkspacedata rename to OpenEdX.xcworkspace/contents.xcworkspacedata index c93c80108..6afcf4628 100644 --- a/NewEdX.xcworkspace/contents.xcworkspacedata +++ b/OpenEdX.xcworkspace/contents.xcworkspacedata @@ -2,7 +2,7 @@ + location = "group:OpenEdX.xcodeproj"> diff --git a/NewEdX.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/OpenEdX.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from NewEdX.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to OpenEdX.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..0c67376eb --- /dev/null +++ b/OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/NewEdX/AnalyticsManager.swift b/OpenEdX/AnalyticsManager.swift similarity index 99% rename from NewEdX/AnalyticsManager.swift rename to OpenEdX/AnalyticsManager.swift index c765cc648..04a11b640 100644 --- a/NewEdX/AnalyticsManager.swift +++ b/OpenEdX/AnalyticsManager.swift @@ -1,6 +1,6 @@ // // AnalyticsManager.swift -// NewEdX +// OpenEdX // // Created by  Stepanok Ivan on 27.06.2023. // diff --git a/NewEdX/AppDelegate.swift b/OpenEdX/AppDelegate.swift similarity index 99% rename from NewEdX/AppDelegate.swift rename to OpenEdX/AppDelegate.swift index 7e8cd9ad1..931f65420 100644 --- a/NewEdX/AppDelegate.swift +++ b/OpenEdX/AppDelegate.swift @@ -1,6 +1,6 @@ // // AppDelegate.swift -// NewEdX +// OpenEdX // // Created by Vladimir Chekyrta on 13.09.2022. // diff --git a/NewEdX/Assets.xcassets/AccentColor.colorset/Contents.json b/OpenEdX/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from NewEdX/Assets.xcassets/AccentColor.colorset/Contents.json rename to OpenEdX/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/NewEdX/Assets.xcassets/AppIcon.appiconset/Contents.json b/OpenEdX/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from NewEdX/Assets.xcassets/AppIcon.appiconset/Contents.json rename to OpenEdX/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/NewEdX/Assets.xcassets/AppIcon.appiconset/app icon.jpg b/OpenEdX/Assets.xcassets/AppIcon.appiconset/app icon.jpg similarity index 100% rename from NewEdX/Assets.xcassets/AppIcon.appiconset/app icon.jpg rename to OpenEdX/Assets.xcassets/AppIcon.appiconset/app icon.jpg diff --git a/NewEdX/Assets.xcassets/Contents.json b/OpenEdX/Assets.xcassets/Contents.json similarity index 100% rename from NewEdX/Assets.xcassets/Contents.json rename to OpenEdX/Assets.xcassets/Contents.json diff --git a/NewEdX/Assets.xcassets/SplachBackground.colorset/Contents.json b/OpenEdX/Assets.xcassets/SplachBackground.colorset/Contents.json similarity index 100% rename from NewEdX/Assets.xcassets/SplachBackground.colorset/Contents.json rename to OpenEdX/Assets.xcassets/SplachBackground.colorset/Contents.json diff --git a/NewEdX/Assets.xcassets/appLogo.imageset/Contents.json b/OpenEdX/Assets.xcassets/appLogo.imageset/Contents.json similarity index 100% rename from NewEdX/Assets.xcassets/appLogo.imageset/Contents.json rename to OpenEdX/Assets.xcassets/appLogo.imageset/Contents.json diff --git a/NewEdX/Assets.xcassets/appLogo.imageset/Group 21.svg b/OpenEdX/Assets.xcassets/appLogo.imageset/Group 21.svg similarity index 100% rename from NewEdX/Assets.xcassets/appLogo.imageset/Group 21.svg rename to OpenEdX/Assets.xcassets/appLogo.imageset/Group 21.svg diff --git a/NewEdX/Base.lproj/LaunchScreen.storyboard b/OpenEdX/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from NewEdX/Base.lproj/LaunchScreen.storyboard rename to OpenEdX/Base.lproj/LaunchScreen.storyboard diff --git a/NewEdX/Base.lproj/languages.json b/OpenEdX/Base.lproj/languages.json similarity index 100% rename from NewEdX/Base.lproj/languages.json rename to OpenEdX/Base.lproj/languages.json diff --git "a/NewEdX/Base.lproj/\321\201ountries.json" "b/OpenEdX/Base.lproj/\321\201ountries.json" similarity index 100% rename from "NewEdX/Base.lproj/\321\201ountries.json" rename to "OpenEdX/Base.lproj/\321\201ountries.json" diff --git a/OpenEdX/Configuration.json b/OpenEdX/Configuration.json new file mode 100644 index 000000000..68f6b7a89 --- /dev/null +++ b/OpenEdX/Configuration.json @@ -0,0 +1,26 @@ +{ + "DebugDev": { + "baseURL": "https://lms-rg-app-ios-dev.raccoongang.com", + "clientId": "T7od4OFlYni7hTMnepfQuF1XUoqsESjEClltL40T" + }, + "ReleaseDev": { + "baseURL": "https://lms-rg-app-ios-dev.raccoongang.com", + "clientId": "T7od4OFlYni7hTMnepfQuF1XUoqsESjEClltL40T" + }, + "DebugStage": { + "baseURL": "https://lms-rg-app-ios-stage.raccoongang.com", + "clientId": "kHDbLaYlc1lpY1obmyAAEp9dX9qPqeDrBiVGQFIy" + }, + "ReleaseStage": { + "baseURL": "https://lms-rg-app-ios-stage.raccoongang.com", + "clientId": "kHDbLaYlc1lpY1obmyAAEp9dX9qPqeDrBiVGQFIy" + }, + "DebugProd": { + "baseURL": "https://example.com", + "clientId": "PROD_CLIENT_ID" + }, + "ReleaseProd": { + "baseURL": "https://example.com", + "clientId": "PROD_CLIENT_ID" + } +} diff --git a/NewEdX/CoreDataHandler.swift b/OpenEdX/CoreDataHandler.swift similarity index 98% rename from NewEdX/CoreDataHandler.swift rename to OpenEdX/CoreDataHandler.swift index da1a19ac7..9746c78fd 100644 --- a/NewEdX/CoreDataHandler.swift +++ b/OpenEdX/CoreDataHandler.swift @@ -1,6 +1,6 @@ // // CoreDataHandler.swift -// NewEdX +// OpenEdX // // Created by  Stepanok Ivan on 09.02.2023. // diff --git a/NewEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift similarity index 99% rename from NewEdX/DI/AppAssembly.swift rename to OpenEdX/DI/AppAssembly.swift index 111f05ab1..2e1dabda0 100644 --- a/NewEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -1,6 +1,6 @@ // // AppAssembly.swift -// NewEdX +// OpenEdX // // Created by Vladimir Chekyrta on 13.09.2022. // diff --git a/NewEdX/DI/NetworkAssembly.swift b/OpenEdX/DI/NetworkAssembly.swift similarity index 99% rename from NewEdX/DI/NetworkAssembly.swift rename to OpenEdX/DI/NetworkAssembly.swift index cf92bed98..f609b5bc5 100644 --- a/NewEdX/DI/NetworkAssembly.swift +++ b/OpenEdX/DI/NetworkAssembly.swift @@ -1,6 +1,6 @@ // // NetworkAssembly.swift -// NewEdX +// OpenEdX // // Created by Vladimir Chekyrta on 13.09.2022. // diff --git a/NewEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift similarity index 99% rename from NewEdX/DI/ScreenAssembly.swift rename to OpenEdX/DI/ScreenAssembly.swift index be302d701..a4b8fa1b9 100644 --- a/NewEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -1,6 +1,6 @@ // // ScreenAssembly.swift -// NewEdX +// OpenEdX // // Created by Vladimir Chekyrta on 14.09.2022. // diff --git a/NewEdX/Environment.swift b/OpenEdX/Environment.swift similarity index 99% rename from NewEdX/Environment.swift rename to OpenEdX/Environment.swift index 8e020c28c..e89c0bb88 100644 --- a/NewEdX/Environment.swift +++ b/OpenEdX/Environment.swift @@ -1,6 +1,6 @@ // // Environment.swift -// NewEdX +// OpenEdX // // Created by Vladimir Chekyrta on 14.09.2022. // diff --git a/NewEdX/Info.plist b/OpenEdX/Info.plist similarity index 86% rename from NewEdX/Info.plist rename to OpenEdX/Info.plist index 66662cd72..b94522839 100644 --- a/NewEdX/Info.plist +++ b/OpenEdX/Info.plist @@ -6,13 +6,13 @@ $(CONFIGURATION) FirebaseAppDelegateProxyEnabled + FirebaseAutomaticScreenReportingEnabled + ITSAppUsesNonExemptEncryption UIAppFonts UIViewControllerBasedStatusBarAppearance - FirebaseAutomaticScreenReportingEnabled - diff --git a/NewEdX/MainScreenAnalytics.swift b/OpenEdX/MainScreenAnalytics.swift similarity index 96% rename from NewEdX/MainScreenAnalytics.swift rename to OpenEdX/MainScreenAnalytics.swift index 8c554dffb..39dd9e484 100644 --- a/NewEdX/MainScreenAnalytics.swift +++ b/OpenEdX/MainScreenAnalytics.swift @@ -1,6 +1,6 @@ // // MainScreenAnalytics.swift -// NewEdX +// OpenEdX // // Created by  Stepanok Ivan on 29.06.2023. // diff --git a/NewEdX/NewEdX.entitlements b/OpenEdX/OpenEdX.entitlements similarity index 100% rename from NewEdX/NewEdX.entitlements rename to OpenEdX/OpenEdX.entitlements diff --git a/NewEdX/RouteController.swift b/OpenEdX/RouteController.swift similarity index 99% rename from NewEdX/RouteController.swift rename to OpenEdX/RouteController.swift index 8c2497e34..a4264ca0a 100644 --- a/NewEdX/RouteController.swift +++ b/OpenEdX/RouteController.swift @@ -1,6 +1,6 @@ // // RouteController.swift -// NewEdX +// OpenEdX // // Created by Vladimir Chekyrta on 13.09.2022. // diff --git a/NewEdX/Router.swift b/OpenEdX/Router.swift similarity index 99% rename from NewEdX/Router.swift rename to OpenEdX/Router.swift index c9f5acb6a..03f48dfee 100644 --- a/NewEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -1,6 +1,6 @@ // // RouterImpl.swift -// NewEdX +// OpenEdX // // Created by Vladimir Chekyrta on 13.09.2022. // diff --git a/NewEdX/SwiftUIHostController.swift b/OpenEdX/SwiftUIHostController.swift similarity index 99% rename from NewEdX/SwiftUIHostController.swift rename to OpenEdX/SwiftUIHostController.swift index 09bcfde30..922edce15 100644 --- a/NewEdX/SwiftUIHostController.swift +++ b/OpenEdX/SwiftUIHostController.swift @@ -1,6 +1,6 @@ // // SwiftUIHostController.swift -// NewEdX +// OpenEdX // // Created by Vladimir Chekyrta on 13.09.2022. // diff --git a/NewEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift similarity index 99% rename from NewEdX/View/MainScreenView.swift rename to OpenEdX/View/MainScreenView.swift index 639efcbf4..fa57435d4 100644 --- a/NewEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -1,6 +1,6 @@ // // MainScreenView.swift -// NewEdX +// OpenEdX // // Created by Vladimir Chekyrta on 15.09.2022. // diff --git a/NewEdX/en.lproj/Localizable.strings b/OpenEdX/en.lproj/Localizable.strings similarity index 88% rename from NewEdX/en.lproj/Localizable.strings rename to OpenEdX/en.lproj/Localizable.strings index ed63e3c25..8e7d62729 100644 --- a/NewEdX/en.lproj/Localizable.strings +++ b/OpenEdX/en.lproj/Localizable.strings @@ -1,6 +1,6 @@ /* Localizable.strings - NewEdX + OpenEdX Created by Vladimir Chekyrta on 13.09.2022. diff --git a/NewEdX/uk.lproj/LaunchScreen.strings b/OpenEdX/uk.lproj/LaunchScreen.strings similarity index 100% rename from NewEdX/uk.lproj/LaunchScreen.strings rename to OpenEdX/uk.lproj/LaunchScreen.strings diff --git a/NewEdX/uk.lproj/Localizable.strings b/OpenEdX/uk.lproj/Localizable.strings similarity index 88% rename from NewEdX/uk.lproj/Localizable.strings rename to OpenEdX/uk.lproj/Localizable.strings index ed63e3c25..8e7d62729 100644 --- a/NewEdX/uk.lproj/Localizable.strings +++ b/OpenEdX/uk.lproj/Localizable.strings @@ -1,6 +1,6 @@ /* Localizable.strings - NewEdX + OpenEdX Created by Vladimir Chekyrta on 13.09.2022. diff --git a/NewEdX/uk.lproj/languages.json b/OpenEdX/uk.lproj/languages.json similarity index 100% rename from NewEdX/uk.lproj/languages.json rename to OpenEdX/uk.lproj/languages.json diff --git "a/NewEdX/uk.lproj/\321\201ountries.json" "b/OpenEdX/uk.lproj/\321\201ountries.json" similarity index 100% rename from "NewEdX/uk.lproj/\321\201ountries.json" rename to "OpenEdX/uk.lproj/\321\201ountries.json" diff --git a/Podfile b/Podfile index 1e80d0312..1d97fa467 100644 --- a/Podfile +++ b/Podfile @@ -8,9 +8,9 @@ abstract_target "App" do #CodeGen for resources pod 'SwiftGen', '~> 6.6' - target "NewEdX" do + target "OpenEdX" do inherit! :complete - workspace './NewEdX.xcodeproj' + workspace './OpenEdX.xcodeproj' end target "Core" do diff --git a/Podfile.lock b/Podfile.lock index 2d715ae54..5b998cd5e 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -180,6 +180,6 @@ SPEC CHECKSUMS: SwiftyMocky: c5e96e4ff76ec6dbf5a5941aeb039b5a546954a0 Swinject: 893c9a543000ac2f10ee4cbaf0933c6992c935d5 -PODFILE CHECKSUM: e535ea6bf82dfd5be3ef0ea79b6907f520e57993 +PODFILE CHECKSUM: 1639b311802f5d36686512914067b7221ff97a64 -COCOAPODS: 1.12.0 +COCOAPODS: 1.12.1 diff --git a/Profile/Profile.xcodeproj.xcworkspace/contents.xcworkspacedata b/Profile/Profile.xcodeproj.xcworkspace/contents.xcworkspacedata index 4159659ea..964f294a7 100644 --- a/Profile/Profile.xcodeproj.xcworkspace/contents.xcworkspacedata +++ b/Profile/Profile.xcodeproj.xcworkspace/contents.xcworkspacedata @@ -14,7 +14,7 @@ location = "group:../Discovery/Discovery.xcodeproj"> + location = "group:../OpenEdX.xcodeproj"> diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index 87c456633..c92a7d9d7 100644 --- a/Profile/Profile.xcodeproj/project.pbxproj +++ b/Profile/Profile.xcodeproj/project.pbxproj @@ -732,7 +732,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Profile; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Profile; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -767,7 +767,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Profile; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Profile; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -864,7 +864,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Profile; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Profile; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -962,7 +962,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Profile; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Profile; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1054,7 +1054,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Profile; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Profile; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1145,7 +1145,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Profile; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Profile; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1163,12 +1163,12 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.ProfileTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1184,12 +1184,12 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.ProfileTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1205,12 +1205,12 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.ProfileTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1226,12 +1226,12 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.ProfileTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1247,12 +1247,12 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.ProfileTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1268,12 +1268,12 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.ProfileTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1368,7 +1368,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Profile; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Profile; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1387,12 +1387,12 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.ProfileTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -1481,7 +1481,7 @@ "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.raccoongang.Profile; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Profile; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1499,12 +1499,12 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = KHU94Q2JSS; + DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = stepanok.ProfileTests; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; diff --git a/README.md b/README.md index d89789884..9a9afef9b 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ Modern vision of the mobile application for the Open EdX platform from Raccoon G 2. Navigate to the project folder and run ``pod install``. -3. Open ``NewEdX.xcworkspace``. +3. Open ``OpenEdX.xcworkspace``. -4. Ensure that the ``NewEdXDev`` or ``NewEdXProd`` scheme is selected. +4. Ensure that the ``OpenEdXDev`` or ``OpenEdXProd`` scheme is selected. -5. Configure the [``Environment.swift`` file](https://github.com/raccoongang/new-edx-app-ios/blob/main/NewEdX/Environment.swift) with URLs and OAuth credentials for your Open edX instance. +5. Configure the [``Environment.swift`` file](https://github.com/raccoongang/new-edx-app-ios/blob/main/OpenEdX/Environment.swift) with URLs and OAuth credentials for your Open edX instance. 6. Click the **Run** button. @@ -26,6 +26,8 @@ You can find the plugin with the API and installation guide [here](https://githu Please feel welcome to develop any of the suggested features below and submit a pull request. - ✅ ~~Migrate to the new APIs~~ +- ✅ ~~New Navigation~~ +- ✅ ~~Analytics and Crashlytics~~ - Recent searches - Migrate to the Olive and JWT token - UnAuth User mode diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ce734301b..aa059c588 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -26,8 +26,8 @@ end lane :unit_tests do run_tests( - workspace: "NewEdX.xcworkspace", + workspace: "OpenEdX.xcworkspace", device: "iPhone 14", - scheme: "NewEdXDev" + scheme: "OpenEdXDev" ) end