From 47a014d489955b6ffdacf633f31f0b2d4acb8a5c Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Fri, 22 Sep 2023 16:15:19 +0200 Subject: [PATCH] Revert "Develop to master v1.3 (#79)" This reverts commit 83ee9bffb246d48619da93f08f31e0feac1c6f48. --- .../Authorization.xcodeproj/project.pbxproj | 32 +- .../Presentation/Base/FieldsView.swift | 1 - .../Presentation/Login/SignInView.swift | 33 +- .../Presentation/Login/SignInViewModel.swift | 25 +- .../Registration/SignUpView.swift | 18 +- .../Registration/SignUpViewModel.swift | 6 +- .../Reset Password/ResetPasswordView.swift | 23 +- .../AuthorizationMock.generated.swift | 15 - .../Login/SignInViewModelTests.swift | 124 +-- .../Register/SignUpViewModelTests.swift | 131 +-- Core/Core.xcodeproj/project.pbxproj | 40 +- .../Profile/done.imageset/Contents.json | 3 - .../arrowLeft.imageset/Contents.json | 3 - .../arrowRight16.imageset/Contents.json | 3 - Core/Core/Configuration/BaseRouter.swift | 4 +- Core/Core/Configuration/Connectivity.swift | 2 +- {OpenEdX => Core/Core}/Data/AppStorage.swift | 31 +- Core/Core/Data/CoreStorage.swift | 17 - .../CoreDataModel.xcdatamodel/contents | 2 +- .../Data/Persistence}/CorePersistence.swift | 76 +- .../Persistence/CorePersistenceProtocol.swift | 25 - .../Core/Data/Repository/AuthRepository.swift | 4 +- Core/Core/Extensions/CGColorExtension.swift | 17 - .../Core/Extensions/CollectionExtension.swift | 2 +- .../Extensions/UIApplicationExtension.swift | 27 - Core/Core/Extensions/ViewExtension.swift | 30 +- Core/Core/Network/DownloadManager.swift | 79 +- Core/Core/Network/RequestInterceptor.swift | 14 +- Core/Core/Theme.swift | 73 -- Core/Core/View/Base/AlertView.swift | 14 +- Core/Core/View/Base/CourseButton.swift | 4 +- Core/Core/View/Base/CourseCellView.swift | 14 +- Core/Core/View/Base/DownloadView.swift | 6 +- .../View/Base/FlexibleKeyboardInputView.swift | 12 +- Core/Core/View/Base/NavigationBar.swift | 12 +- Core/Core/View/Base/OfflineSnackBarView.swift | 2 +- Core/Core/View/Base/PickerMenu.swift | 12 +- Core/Core/View/Base/PickerView.swift | 10 +- Core/Core/View/Base/ProgressBar.swift | 6 +- .../View/Base/RefreshableScrollView.swift | 81 ++ .../Base/RefreshableScrollViewCompat.swift | 13 +- .../View/Base/RegistrationTextField.swift | 16 +- Core/Core/View/Base/SnackBarView.swift | 2 +- Core/Core/View/Base/StyledButton.swift | 12 +- Core/Core/View/Base/UnitButtonView.swift | 36 +- Core/Core/View/Base/WebBrowser.swift | 20 +- Core/Core/View/Base/WebUnitView.swift | 6 +- Core/Core/View/Base/WebView.swift | 15 +- Course/Course.xcodeproj/project.pbxproj | 48 +- Course/Course/Data/CourseRepository.swift | 20 +- .../Model/Data_CourseOutlineResponse.swift | 94 +- .../Data/Model/Data_UpdatesResponse.swift | 2 +- ...oint.swift => CourseDetailsEndpoint.swift} | 4 +- .../Data/Persistence}/CoursePersistence.swift | 82 +- .../CoursePersistenceProtocol.swift | 25 - .../Course/Domain/Model/CourseDetails.swift | 24 +- Course/Course/Domain/Model/CourseUpdate.swift | 4 +- .../Container/CourseContainerView.swift | 37 +- .../Details/CourseDetailsView.swift | 27 +- .../Handouts/HandoutsUpdatesDetailView.swift | 113 +-- .../Presentation/Handouts/HandoutsView.swift | 22 +- .../Outline/ContinueWithView.swift | 6 +- .../Outline/CourseOutlineView.swift | 39 +- .../Outline/CourseVerticalView.swift | 199 ++-- .../Presentation/Unit/CourseUnitView.swift | 169 ++-- .../Unit/Subviews/LessonProgressView.swift | 2 +- .../Unit/Subviews/YouTubeView.swift | 2 +- .../Video/EncodedVideoPlayer.swift | 2 +- .../Video/PlayerViewController.swift | 7 - .../Presentation/Video/SubtittlesView.swift | 6 +- .../Video/VideoPlayerViewModel.swift | 2 +- .../Video/YouTubeVideoPlayer.swift | 2 +- Course/CourseTests/CourseMock.generated.swift | 15 - Dashboard/Dashboard.xcodeproj/project.pbxproj | 40 +- .../Dashboard/Data/DashboardRepository.swift | 8 +- .../Data/Network/DashboardEndpoint.swift | 8 +- .../DashboardCoreModel.xcdatamodel/contents | 4 +- .../Persistence/DashboardPersistence.swift | 120 +++ .../DashboardPersistenceProtocol.swift | 18 - .../Presentation/DashboardView.swift | 43 +- .../Presentation/DashboardViewModel.swift | 2 +- .../DashboardMock.generated.swift | 15 - Discovery/Discovery.xcodeproj/project.pbxproj | 40 +- .../Discovery/Data/DiscoveryRepository.swift | 4 +- .../DiscoveryCoreModel.xcdatamodel/contents | 4 +- .../Persistence/DiscoveryPersistence.swift | 122 +++ .../DiscoveryPersistenceProtocol.swift | 18 - .../Presentation/DiscoveryView.swift | 88 +- .../Presentation/DiscoveryViewModel.swift | 16 +- .../Discovery/Presentation/SearchView.swift | 29 +- .../DiscoveryMock.generated.swift | 15 - .../Discussion.xcodeproj/project.pbxproj | 32 +- .../Data/Model/Data_CreatedComment.swift | 4 +- .../Data/Network/DiscussionRepository.swift | 4 +- .../Presentation/CheckBoxView.swift | 4 +- .../Base/BaseResponsesViewModel.swift | 14 +- .../Comments/Base/CommentCell.swift | 20 +- .../Comments/Base/ParentCommentView.swift | 18 +- .../Comments/Responses/ResponsesView.swift | 261 +++--- .../Responses/ResponsesViewModel.swift | 9 +- .../Comments/Thread/ThreadView.swift | 305 ++++--- .../Comments/Thread/ThreadViewModel.swift | 15 +- .../CreateNewThread/CreateNewThreadView.swift | 29 +- .../DiscussionSearchTopicsView.swift | 30 +- .../DiscussionTopicsView.swift | 61 +- .../Presentation/Posts/PostsView.swift | 290 +++--- .../Presentation/Posts/PostsViewModel.swift | 102 ++- .../DiscussionMock.generated.swift | 15 - .../Base/BaseResponsesViewModelTests.swift | 40 +- .../Comment/ThreadViewModelTests.swift | 8 + .../Posts/PostViewModelTests.swift | 26 +- .../Responses/ResponsesViewModelTests.swift | 7 + LICENSE | 862 ++++++++++++++---- OpenEdX.xcodeproj/project.pbxproj | 52 +- OpenEdX/AppDelegate.swift | 3 +- OpenEdX/CoreDataHandler.swift | 35 + OpenEdX/DI/AppAssembly.swift | 22 +- OpenEdX/DI/NetworkAssembly.swift | 2 +- OpenEdX/DI/ScreenAssembly.swift | 36 +- OpenEdX/Data/DashboardPersistence.swift | 66 -- OpenEdX/Data/DatabaseManager.swift | 84 -- OpenEdX/Data/DiscoveryPersistence.swift | 69 -- OpenEdX/RouteController.swift | 11 +- OpenEdX/Router.swift | 49 +- OpenEdX/SwiftUIHostController.swift | 50 + OpenEdX/View/MainScreenView.swift | 69 +- OpenEdX/uk.lproj/languages.json | 2 +- Profile/Data/ProfileStorage.swift | 22 - Profile/Profile.xcodeproj/project.pbxproj | 44 +- Profile/Profile/Data/ProfileRepository.swift | 41 +- .../DeleteAccount/DeleteAccountView.swift | 189 ++-- .../EditProfile/EditProfileView.swift | 277 +++--- .../EditProfile/EditProfileViewModel.swift | 35 +- .../EditProfile/ProfileBottomSheet.swift | 16 +- .../Presentation/Profile/ProfileView.swift | 386 ++++---- .../Profile/ProfileViewModel.swift | 43 +- .../Presentation/Settings/SettingsView.swift | 95 +- .../Settings/VideoQualityView.swift | 82 +- .../EditProfileViewModelTests.swift | 62 -- .../Profile/ProfileViewModelTests.swift | 234 ++--- .../ProfileTests/ProfileMock.generated.swift | 15 - README.md | 18 +- ...1-strategy-for-maintaining-OS-versions.rst | 62 -- 143 files changed, 3464 insertions(+), 3563 deletions(-) rename {OpenEdX => Core/Core}/Data/AppStorage.swift (92%) delete mode 100644 Core/Core/Data/CoreStorage.swift rename {OpenEdX/Data => Core/Core/Data/Persistence}/CorePersistence.swift (76%) delete mode 100644 Core/Core/Data/Persistence/CorePersistenceProtocol.swift rename Course/Course/Data/Network/{CourseEndpoint.swift => CourseDetailsEndpoint.swift} (98%) rename {OpenEdX/Data => Course/Course/Data/Persistence}/CoursePersistence.swift (76%) delete mode 100644 Course/Course/Data/Persistence/CoursePersistenceProtocol.swift create mode 100644 Dashboard/Dashboard/Data/Persistence/DashboardPersistence.swift delete mode 100644 Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift create mode 100644 Discovery/Discovery/Data/Persistence/DiscoveryPersistence.swift delete mode 100644 Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift create mode 100644 OpenEdX/CoreDataHandler.swift delete mode 100644 OpenEdX/Data/DashboardPersistence.swift delete mode 100644 OpenEdX/Data/DatabaseManager.swift delete mode 100644 OpenEdX/Data/DiscoveryPersistence.swift create mode 100644 OpenEdX/SwiftUIHostController.swift delete mode 100644 Profile/Data/ProfileStorage.swift delete mode 100644 docs/0001-strategy-for-maintaining-OS-versions.rst diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index fba945920..b4c539d33 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -583,7 +583,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -611,7 +611,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -694,7 +694,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -721,7 +721,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -739,7 +739,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -757,7 +757,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -775,7 +775,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -793,7 +793,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -811,7 +811,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -829,7 +829,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -918,7 +918,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1011,7 +1011,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1109,7 +1109,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1202,7 +1202,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1358,7 +1358,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1393,7 +1393,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Authorization/Authorization/Presentation/Base/FieldsView.swift b/Authorization/Authorization/Presentation/Base/FieldsView.swift index 1a71a2483..c021b565c 100644 --- a/Authorization/Authorization/Presentation/Base/FieldsView.swift +++ b/Authorization/Authorization/Presentation/Base/FieldsView.swift @@ -63,7 +63,6 @@ struct FieldsView: View { type: .discovery, fontSize: 90, screenWidth: proxy.size.width) ) - .id(UUID()) .padding(.horizontal, -6) case .unknown: diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index fd98fde7c..98fc3305a 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -39,16 +39,16 @@ public struct SignInView: View { VStack(alignment: .leading) { Text(AuthLocalization.SignIn.logInTitle) .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.bottom, 4) Text(AuthLocalization.SignIn.welcomeBack) .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.bottom, 20) Text(AuthLocalization.SignIn.email) .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) TextField(AuthLocalization.SignIn.email, text: $email) .keyboardType(.emailAddress) .textContentType(.emailAddress) @@ -57,42 +57,42 @@ public struct SignInView: View { .padding(.all, 14) .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputStroke) + .fill(CoreAssets.textInputStroke.swiftUIColor) ) Text(AuthLocalization.SignIn.password) .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.top, 18) SecureField(AuthLocalization.SignIn.password, text: $password) .padding(.all, 14) .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputStroke) + .fill(CoreAssets.textInputStroke.swiftUIColor) ) HStack { Button(AuthLocalization.SignIn.registerBtn) { - viewModel.trackSignUpClicked() + viewModel.analytics.signUpClicked() viewModel.router.showRegisterScreen() - }.foregroundColor(Theme.Colors.accentColor) + }.foregroundColor(CoreAssets.accentColor.swiftUIColor) Spacer() Button(AuthLocalization.SignIn.forgotPassBtn) { - viewModel.trackForgotPasswordClicked() + viewModel.analytics.forgotPasswordClicked() viewModel.router.showForgotPasswordScreen() - }.foregroundColor(Theme.Colors.accentColor) + }.foregroundColor(CoreAssets.accentColor.swiftUIColor) } .padding(.top, 10) if viewModel.isShowProgress { @@ -113,7 +113,7 @@ public struct SignInView: View { } .padding(.horizontal, 24) .padding(.top, 50) - }.roundedBackground(Theme.Colors.background) + }.roundedBackground(CoreAssets.background.swiftUIColor) .scrollAvoidKeyboard(dismissKeyboardByTap: true) } @@ -122,7 +122,7 @@ public struct SignInView: View { if viewModel.showAlert { VStack { Text(viewModel.alertMessage ?? "") - .shadowCardStyle(bgColor: Theme.Colors.accentColor, + .shadowCardStyle(bgColor: CoreAssets.accentColor.swiftUIColor, textColor: .white) .padding(.top, 80) Spacer() @@ -149,10 +149,7 @@ public struct SignInView: View { } } } - .hideNavigationBar() - .navigationBarBackButtonHidden(true) - .navigationBarHidden(true) - .background(Theme.Colors.background.ignoresSafeArea(.all)) + .background(CoreAssets.background.swiftUIColor.ignoresSafeArea(.all)) } } diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index 6d8ebfdee..91a136958 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -30,24 +30,21 @@ public class SignInViewModel: ObservableObject { } } - let router: AuthorizationRouter - private let interactor: AuthInteractorProtocol - private let analytics: AuthorizationAnalytics + let router: AuthorizationRouter + let analytics: AuthorizationAnalytics private let validator: Validator - public init( - interactor: AuthInteractorProtocol, - router: AuthorizationRouter, - analytics: AuthorizationAnalytics, - validator: Validator - ) { + public init(interactor: AuthInteractorProtocol, + router: AuthorizationRouter, + analytics: AuthorizationAnalytics, + validator: Validator) { self.interactor = interactor self.router = router self.analytics = analytics self.validator = validator } - + @MainActor func login(username: String, password: String) async { guard validator.isValidEmail(username) else { @@ -79,12 +76,4 @@ public class SignInViewModel: ObservableObject { } } } - - func trackSignUpClicked() { - analytics.signUpClicked() - } - - func trackForgotPasswordClicked() { - analytics.forgotPasswordClicked() - } } diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 2ce5f263c..6cace79ba 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -2,7 +2,7 @@ // SignUpView.swift // Authorization // -// Created by Stepanok Ivan on 24.10.2022. +// Created by  Stepanok Ivan on 24.10.2022. // import SwiftUI @@ -43,11 +43,12 @@ public struct SignUpView: View { CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) .backButtonStyle(color: .white) }) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) }.frame(minWidth: 0, maxWidth: .infinity, alignment: .topLeading) + .frameLimit() } GeometryReader { proxy in @@ -57,11 +58,11 @@ public struct SignUpView: View { Text(AuthLocalization.SignUp.title) .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.bottom, 4) Text(AuthLocalization.SignUp.subtitle) .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.bottom, 20) let requiredFields = viewModel.fields.filter {$0.field.required} @@ -79,7 +80,7 @@ public struct SignUpView: View { router: viewModel.router, configuration: viewModel.config, cssInjector: viewModel.cssInjector, - proxy: proxy).padding(.horizontal, 1) + proxy: proxy) }, label: { Text(disclosureGroupOpen ? AuthLocalization.SignUp.hideFields @@ -95,9 +96,9 @@ public struct SignUpView: View { } else { StyledButton(AuthLocalization.SignUp.createAccountBtn) { Task { + viewModel.analytics.createAccountClicked() await viewModel.registerUser() } - viewModel.trackCreateAccountClicked() } .padding(.top, 40) .padding(.bottom, 80) @@ -108,7 +109,7 @@ public struct SignUpView: View { .padding(.horizontal, 24) .padding(.top, 24) - }.roundedBackground(Theme.Colors.background) + }.roundedBackground(CoreAssets.background.swiftUIColor) .onRightSwipeGesture { viewModel.router.back() } @@ -135,8 +136,7 @@ public struct SignUpView: View { } } } - .background(Theme.Colors.background.ignoresSafeArea(.all)) - .hideNavigationBar() + .background(CoreAssets.background.swiftUIColor.ignoresSafeArea(.all)) } } diff --git a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift index a2142684f..c929c80c3 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift @@ -25,11 +25,11 @@ public class SignUpViewModel: ObservableObject { @Published var fields: [FieldConfiguration] = [] let router: AuthorizationRouter + let analytics: AuthorizationAnalytics let config: Config let cssInjector: CSSInjector private let interactor: AuthInteractorProtocol - private let analytics: AuthorizationAnalytics private let validator: Validator public init( @@ -106,8 +106,4 @@ public class SignUpViewModel: ObservableObject { } } } - - func trackCreateAccountClicked() { - analytics.createAccountClicked() - } } diff --git a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift index 17f7466c0..17d81921d 100644 --- a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift +++ b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift @@ -2,7 +2,7 @@ // ResetPasswordView.swift // Authorization // -// Created by Stepanok Ivan on 27.03.2023. +// Created by  Stepanok Ivan on 27.03.2023. // import SwiftUI @@ -51,12 +51,12 @@ public struct ResetPasswordView: View { Text(AuthLocalization.Forgot.checkTitle) .font(Theme.Fonts.titleLarge) .multilineTextAlignment(.center) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.bottom, 4) Text(AuthLocalization.Forgot.checkDescription + email) .font(Theme.Fonts.bodyMedium) .multilineTextAlignment(.center) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.bottom, 20) StyledButton(AuthLocalization.SignIn.logInBtn) { viewModel.router.backToRoot(animated: true) @@ -70,15 +70,15 @@ public struct ResetPasswordView: View { VStack(alignment: .leading) { Text(AuthLocalization.Forgot.title) .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.bottom, 4) Text(AuthLocalization.Forgot.description) .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.bottom, 20) Text(AuthLocalization.SignIn.email) .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) TextField(AuthLocalization.SignIn.email, text: $email) .keyboardType(.emailAddress) .textContentType(.emailAddress) @@ -87,12 +87,12 @@ public struct ResetPasswordView: View { .padding(.all, 14) .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputStroke) + .fill(CoreAssets.textInputStroke.swiftUIColor) ) if viewModel.isShowProgress { HStack(alignment: .center) { @@ -113,7 +113,7 @@ public struct ResetPasswordView: View { } .padding(.horizontal, 24) .padding(.top, 50) - }.roundedBackground(Theme.Colors.background) + }.roundedBackground(CoreAssets.background.swiftUIColor) .scrollAvoidKeyboard(dismissKeyboardByTap: true) } @@ -122,7 +122,7 @@ public struct ResetPasswordView: View { if viewModel.showAlert { VStack { Text(viewModel.alertMessage ?? "") - .shadowCardStyle(bgColor: Theme.Colors.accentColor, + .shadowCardStyle(bgColor: CoreAssets.accentColor.swiftUIColor, textColor: .white) .padding(.top, 80) Spacer() @@ -149,8 +149,7 @@ public struct ResetPasswordView: View { } } } - .background(Theme.Colors.background.ignoresSafeArea(.all)) - .hideNavigationBar() + .background(CoreAssets.background.swiftUIColor.ignoresSafeArea(.all)) } } diff --git a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift index ddb4ac259..b47ea117a 100644 --- a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift +++ b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift @@ -1786,12 +1786,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { perform?(`blocks`) } - open func deleteAllFiles() { - addInvocation(.m_deleteAllFiles) - let perform = methodPerformValue(.m_deleteAllFiles) as? () -> Void - perform?() - } - open func fileUrl(for blockId: String) -> URL? { addInvocation(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) let perform = methodPerformValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void @@ -1814,7 +1808,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_resumeDownloading case m_pauseDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) - case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -1846,8 +1839,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_deleteAllFiles, .m_deleteAllFiles): return .match - case (.m_fileUrl__for_blockId(let lhsBlockid), .m_fileUrl__for_blockId(let rhsBlockid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) @@ -1865,7 +1856,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return 0 case .m_pauseDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue - case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue } } @@ -1878,7 +1868,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return ".resumeDownloading()" case .m_pauseDownloading: return ".pauseDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" - case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" } } @@ -1965,7 +1954,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} - public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} } @@ -1994,9 +1982,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } - public static func deleteAllFiles(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_deleteAllFiles, performs: perform) - } public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } diff --git a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift index d478f0f68..2fa56145d 100644 --- a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift @@ -13,26 +13,24 @@ import Alamofire import SwiftUI final class SignInViewModelTests: XCTestCase { - + override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. } - + override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } - + func testLoginValidationEmailError() async throws { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignInViewModel( - interactor: interactor, - router: router, - analytics: analytics, - validator: validator - ) + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) await viewModel.login(username: "email", password: "") @@ -48,12 +46,10 @@ final class SignInViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignInViewModel( - interactor: interactor, - router: router, - analytics: analytics, - validator: validator - ) + 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)) @@ -68,12 +64,10 @@ final class SignInViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignInViewModel( - interactor: interactor, - router: router, - analytics: analytics, - validator: validator - ) + 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)) @@ -93,12 +87,10 @@ final class SignInViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignInViewModel( - interactor: interactor, - router: router, - analytics: analytics, - validator: validator - ) + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) let validationErrorMessage = "Some error" let validationError = CustomValidationError(statusCode: 400, data: ["error_description": validationErrorMessage]) @@ -120,12 +112,10 @@ final class SignInViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignInViewModel( - interactor: interactor, - router: router, - analytics: analytics, - validator: validator - ) + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) Given(interactor, .login(username: .any, password: .any, willThrow: APIError.invalidGrant)) @@ -143,20 +133,18 @@ final class SignInViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignInViewModel( - interactor: interactor, - router: router, - analytics: analytics, - validator: validator - ) + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) Given(interactor, .login(username: .any, password: .any, willThrow: NSError())) - + await viewModel.login(username: "edxUser@edx.com", password: "password123") - + Verify(interactor, 1, .login(username: .any, password: .any)) Verify(router, 0, .showMainScreen()) - + XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) XCTAssertEqual(viewModel.isShowProgress, false) } @@ -166,58 +154,22 @@ final class SignInViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignInViewModel( - interactor: interactor, - router: router, - analytics: analytics, - validator: validator - ) + let viewModel = SignInViewModel(interactor: interactor, + router: router, + analytics: analytics, + validator: validator) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) Given(interactor, .login(username: .any, password: .any, willThrow: noInternetError)) - + await viewModel.login(username: "edxUser@edx.com", password: "password123") - + Verify(interactor, 1, .login(username: .any, password: .any)) Verify(router, 0, .showMainScreen()) - + XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) XCTAssertEqual(viewModel.isShowProgress, false) } - - func testTrackSignUpClicked() { - let interactor = AuthInteractorProtocolMock() - let router = AuthorizationRouterMock() - let validator = Validator() - let analytics = AuthorizationAnalyticsMock() - let viewModel = SignInViewModel( - interactor: interactor, - router: router, - analytics: analytics, - validator: validator - ) - - viewModel.trackSignUpClicked() - - Verify(analytics, 1, .signUpClicked()) - } - - func testTrackForgotPasswordClicked() { - let interactor = AuthInteractorProtocolMock() - let router = AuthorizationRouterMock() - let validator = Validator() - let analytics = AuthorizationAnalyticsMock() - let viewModel = SignInViewModel( - interactor: interactor, - router: router, - analytics: analytics, - validator: validator - ) - - viewModel.trackForgotPasswordClicked() - - Verify(analytics, 1, .forgotPasswordClicked()) - } - + } diff --git a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift index 8699b79fc..ee463f6ef 100644 --- a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift @@ -27,14 +27,12 @@ final class SignUpViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignUpViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - cssInjector: CSSInjectorMock(), - validator: validator - ) + let viewModel = SignUpViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + cssInjector: CSSInjectorMock(), + validator: validator) let fields = [ PickerFields(type: .email, label: "", required: true, name: "email", instructions: "", options: []), @@ -58,14 +56,12 @@ final class SignUpViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignUpViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - cssInjector: CSSInjectorMock(), - validator: validator - ) + let viewModel = SignUpViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + cssInjector: CSSInjectorMock(), + validator: validator) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -84,14 +80,12 @@ final class SignUpViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignUpViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - cssInjector: CSSInjectorMock(), - validator: validator - ) + let viewModel = SignUpViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + cssInjector: CSSInjectorMock(), + validator: validator) Given(interactor, .getRegistrationFields(willThrow: NSError())) @@ -108,14 +102,12 @@ final class SignUpViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignUpViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - cssInjector: CSSInjectorMock(), - validator: validator - ) + let viewModel = SignUpViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + cssInjector: CSSInjectorMock(), + validator: validator) Given(interactor, .registerUser(fields: .any, willReturn: .init(id: 1, username: "Name", @@ -139,14 +131,12 @@ final class SignUpViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignUpViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - cssInjector: CSSInjectorMock(), - validator: validator - ) + let viewModel = SignUpViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + cssInjector: CSSInjectorMock(), + validator: validator) viewModel.fields = [ FieldConfiguration(field: .init(type: .email, @@ -176,14 +166,12 @@ final class SignUpViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignUpViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - cssInjector: CSSInjectorMock(), - validator: validator - ) + let viewModel = SignUpViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + cssInjector: CSSInjectorMock(), + validator: validator) Given(interactor, .validateRegistrationFields(fields: .any, willReturn: [:])) Given(interactor, .registerUser(fields: .any, willThrow: APIError.invalidGrant)) @@ -204,14 +192,12 @@ final class SignUpViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignUpViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - cssInjector: CSSInjectorMock(), - validator: validator - ) + let viewModel = SignUpViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + cssInjector: CSSInjectorMock(), + validator: validator) Given(interactor, .validateRegistrationFields(fields: .any, willReturn: [:])) Given(interactor, .registerUser(fields: .any, willThrow: NSError())) @@ -232,14 +218,12 @@ final class SignUpViewModelTests: XCTestCase { let router = AuthorizationRouterMock() let validator = Validator() let analytics = AuthorizationAnalyticsMock() - let viewModel = SignUpViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - cssInjector: CSSInjectorMock(), - validator: validator - ) + let viewModel = SignUpViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + cssInjector: CSSInjectorMock(), + validator: validator) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -256,23 +240,4 @@ final class SignUpViewModelTests: XCTestCase { XCTAssertEqual(viewModel.showError, true) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) } - - func testTrackCreateAccountClicked() { - 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 - ) - - viewModel.trackCreateAccountClicked() - - Verify(analytics, 1, .createAccountClicked()) - } } diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index eed739e2e..a23953164 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 024D865E28F02C6B0077E0A0 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024D865D28F02C6B0077E0A0 /* WebView.swift */; }; 024FCD0028EF1CD300232339 /* WebBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024FCCFF28EF1CD300232339 /* WebBrowser.swift */; }; 02512FF0299533DF0024D438 /* CoreDataHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02512FEF299533DE0024D438 /* CoreDataHandlerProtocol.swift */; }; + 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 */; }; @@ -62,13 +63,12 @@ 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 */; }; - 02A4833529B8A73400D33F33 /* CorePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.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 */; }; 02A4833C29B8C57800D33F33 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833B29B8C57800D33F33 /* DownloadView.swift */; }; 02B2B594295C5C7A00914876 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B2B593295C5C7A00914876 /* Thread.swift */; }; 02B3E3B32930198600A50475 /* AVPlayerViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */; }; - 02B3F16E2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */; }; 02C2DC0829B63D6200F4445D /* WebViewHTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C2DC0729B63D6200F4445D /* WebViewHTML.swift */; }; 02C917F029CDA99E00DBB8BD /* Data_Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */; }; 02CF46C829546AA200A698EE /* NoCachedDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CF46C729546AA200A698EE /* NoCachedDataError.swift */; }; @@ -99,7 +99,7 @@ 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 /* CoreStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2428D08FBA006D8A5D /* CoreStorage.swift */; }; + 0770DE2528D08FBA006D8A5D /* AppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2428D08FBA006D8A5D /* AppStorage.swift */; }; 0770DE2A28D0929E006D8A5D /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2928D0929E006D8A5D /* HTTPTask.swift */; }; 0770DE2C28D092B3006D8A5D /* NetworkLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2B28D092B3006D8A5D /* NetworkLogger.swift */; }; 0770DE2E28D09743006D8A5D /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2D28D09743006D8A5D /* API.swift */; }; @@ -152,6 +152,7 @@ 024D865D28F02C6B0077E0A0 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; 024FCCFF28EF1CD300232339 /* WebBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebBrowser.swift; sourceTree = ""; }; 02512FEF299533DE0024D438 /* CoreDataHandlerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandlerProtocol.swift; sourceTree = ""; }; + 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 = ""; }; @@ -181,13 +182,12 @@ 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 = ""; }; - 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePersistenceProtocol.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 = ""; }; 02A4833B29B8C57800D33F33 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; }; 02B2B593295C5C7A00914876 /* Thread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thread.swift; sourceTree = ""; }; 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerViewControllerExtension.swift; sourceTree = ""; }; - 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollViewCompat.swift; sourceTree = ""; }; 02C2DC0729B63D6200F4445D /* WebViewHTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewHTML.swift; sourceTree = ""; }; 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Dashboard.swift; sourceTree = ""; }; 02CF46C729546AA200A698EE /* NoCachedDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCachedDataError.swift; sourceTree = ""; }; @@ -221,7 +221,7 @@ 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 /* CoreStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreStorage.swift; sourceTree = ""; }; + 0770DE2428D08FBA006D8A5D /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = ""; }; 0770DE2928D0929E006D8A5D /* HTTPTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTask.swift; sourceTree = ""; }; 0770DE2B28D092B3006D8A5D /* NetworkLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkLogger.swift; sourceTree = ""; }; 0770DE2D28D09743006D8A5D /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; @@ -357,7 +357,7 @@ isa = PBXGroup; children = ( 02A4833629B8A8F800D33F33 /* CoreDataModel.xcdatamodeld */, - 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.swift */, + 02A4833429B8A73400D33F33 /* CorePersistence.swift */, 02CF46C729546AA200A698EE /* NoCachedDataError.swift */, ); path = Persistence; @@ -381,7 +381,7 @@ 02CF46C92954A42100A698EE /* Persistence */, 0236961728F9A21600EEF206 /* Repository */, 0727877528D2383C002E9142 /* Model */, - 0770DE2428D08FBA006D8A5D /* CoreStorage.swift */, + 0770DE2428D08FBA006D8A5D /* AppStorage.swift */, 02512FEF299533DE0024D438 /* CoreDataHandlerProtocol.swift */, ); path = Data; @@ -521,12 +521,12 @@ 02F6EF3A28D9B8EC00835477 /* CourseCellView.swift */, 024FCCFF28EF1CD300232339 /* WebBrowser.swift */, 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */, + 0251ED0B299D16BC00E70450 /* RefreshableScrollViewCompat.swift */, 023A4DD3299E66BD006C0E48 /* OfflineSnackBarView.swift */, 024D865D28F02C6B0077E0A0 /* WebView.swift */, 02C2DC0729B63D6200F4445D /* WebViewHTML.swift */, 021D925628DCF12900ACC565 /* AlertView.swift */, 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */, - 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */, 0236F3B628F4351E0050F09B /* CourseButton.swift */, 0241666A28F5A78B00082765 /* HTMLFormattedText.swift */, 0282DA7228F98CC9003C3F07 /* WebUnitView.swift */, @@ -775,7 +775,7 @@ 028F9F37293A44C700DE65D0 /* Data_ResetPassword.swift in Sources */, 022C64E429AE0191000F532B /* TextWithUrls.swift in Sources */, 0283348028D4DCD200C828FC /* ViewExtension.swift in Sources */, - 02A4833529B8A73400D33F33 /* CorePersistenceProtocol.swift in Sources */, + 02A4833529B8A73400D33F33 /* CorePersistence.swift in Sources */, 02512FF0299533DF0024D438 /* CoreDataHandlerProtocol.swift in Sources */, 0260E58028FD792800BBBE18 /* WebUnitViewModel.swift in Sources */, 02A4833A29B8A9AB00D33F33 /* DownloadManager.swift in Sources */, @@ -793,7 +793,7 @@ 021D925728DCF12900ACC565 /* AlertView.swift in Sources */, 027BD3A82909474200392132 /* KeyboardAvoidingViewController.swift in Sources */, 0770DE7B28D0C78C006D8A5D /* Theme.swift in Sources */, - 0770DE2528D08FBA006D8A5D /* CoreStorage.swift in Sources */, + 0770DE2528D08FBA006D8A5D /* AppStorage.swift in Sources */, 020306CC2932C0C4000949EA /* PickerView.swift in Sources */, 027BD3C52909707700392132 /* Shake.swift in Sources */, 027BD39C2908810C00392132 /* RegisterUser.swift in Sources */, @@ -802,6 +802,7 @@ 027BD3B42909475900392132 /* KeyboardState.swift in Sources */, 027BD3922907D88F00392132 /* Data_RegistrationFields.swift in Sources */, 07460FE3294B72D700F70538 /* Notification.swift in Sources */, + 0251ED0C299D16BD00E70450 /* RefreshableScrollViewCompat.swift in Sources */, 0727877F28D25B24002E9142 /* Alamofire+Error.swift in Sources */, 02A4833829B8A8F900D33F33 /* CoreDataModel.xcdatamodeld in Sources */, 0259104A29C4A5B6004B5A55 /* UserSettings.swift in Sources */, @@ -824,7 +825,6 @@ 028CE96929858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift in Sources */, 027BD3A92909474200392132 /* KeyboardAvoidingViewControllerRepr.swift in Sources */, 02F98A7F28F81EE900DE94C0 /* Container+App.swift in Sources */, - 02B3F16E2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift in Sources */, 0727877B28D24A1D002E9142 /* HeadersRedirectHandler.swift in Sources */, 0236961B28F9A28B00EEF206 /* AuthInteractor.swift in Sources */, 0770DE3028D09793006D8A5D /* EndPointType.swift in Sources */, @@ -951,7 +951,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1064,7 +1064,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1302,7 +1302,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1395,7 +1395,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1493,7 +1493,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1586,7 +1586,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1742,7 +1742,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1777,7 +1777,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Core/Core/Assets.xcassets/Profile/done.imageset/Contents.json b/Core/Core/Assets.xcassets/Profile/done.imageset/Contents.json index f664a70cf..7706af200 100644 --- a/Core/Core/Assets.xcassets/Profile/done.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/Profile/done.imageset/Contents.json @@ -8,8 +8,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/Core/Core/Assets.xcassets/arrowLeft.imageset/Contents.json b/Core/Core/Assets.xcassets/arrowLeft.imageset/Contents.json index cfa90a49f..90cefb924 100644 --- a/Core/Core/Assets.xcassets/arrowLeft.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/arrowLeft.imageset/Contents.json @@ -18,8 +18,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "original" } } diff --git a/Core/Core/Assets.xcassets/arrowRight16.imageset/Contents.json b/Core/Core/Assets.xcassets/arrowRight16.imageset/Contents.json index 2d22dfa63..cf5254936 100644 --- a/Core/Core/Assets.xcassets/arrowRight16.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/arrowRight16.imageset/Contents.json @@ -8,8 +8,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/Core/Core/Configuration/BaseRouter.swift b/Core/Core/Configuration/BaseRouter.swift index c86b90f62..c6c54ca67 100644 --- a/Core/Core/Configuration/BaseRouter.swift +++ b/Core/Core/Configuration/BaseRouter.swift @@ -28,7 +28,7 @@ public protocol BaseRouter { func showRegisterScreen() func showForgotPasswordScreen() - + func presentAlert( alertTitle: String, alertMessage: String, @@ -88,7 +88,7 @@ open class BaseRouterMock: BaseRouter { public func backWithFade() {} public func removeLastView(controllers: Int) {} - + public func presentAlert( alertTitle: String, alertMessage: String, diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index c1ea53d82..b825a9ddb 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -30,7 +30,7 @@ public class Connectivity: ConnectivityProtocol { public var isMobileData: Bool { if let networkManager { - return networkManager.isReachableOnCellular + return !networkManager.isReachableOnCellular && networkManager.isReachableOnCellular } else { return false } diff --git a/OpenEdX/Data/AppStorage.swift b/Core/Core/Data/AppStorage.swift similarity index 92% rename from OpenEdX/Data/AppStorage.swift rename to Core/Core/Data/AppStorage.swift index 99144be00..ee8bccccb 100644 --- a/OpenEdX/Data/AppStorage.swift +++ b/Core/Core/Data/AppStorage.swift @@ -1,16 +1,14 @@ // // AppStorage.swift -// OpenEdX +// Core // -// Created by  Stepanok Ivan on 31.08.2023. +// Created by Vladimir Chekyrta on 13.09.2022. // import Foundation import KeychainSwift -import Core -import Profile -public class AppStorage: CoreStorage, ProfileStorage { +public class AppStorage { private let keychain: KeychainSwift private let userDefaults: UserDefaults @@ -19,7 +17,7 @@ public class AppStorage: CoreStorage, ProfileStorage { self.keychain = keychain self.userDefaults = userDefaults } - + public var accessToken: String? { get { return keychain.get(KEY_ACCESS_TOKEN) @@ -32,7 +30,7 @@ public class AppStorage: CoreStorage, ProfileStorage { } } } - + public var refreshToken: String? { get { return keychain.get(KEY_REFRESH_TOKEN) @@ -45,7 +43,7 @@ public class AppStorage: CoreStorage, ProfileStorage { } } } - + public var cookiesDate: String? { get { return userDefaults.string(forKey: KEY_COOKIES_DATE) @@ -58,7 +56,7 @@ public class AppStorage: CoreStorage, ProfileStorage { } } } - + public var userProfile: DataLayer.UserProfile? { get { guard let userJson = userDefaults.data(forKey: KEY_USER_PROFILE) else { @@ -77,7 +75,7 @@ public class AppStorage: CoreStorage, ProfileStorage { } } } - + public var userSettings: UserSettings? { get { guard let userSettings = userDefaults.data(forKey: KEY_SETTINGS) else { @@ -101,7 +99,7 @@ public class AppStorage: CoreStorage, ProfileStorage { } } } - + public var user: DataLayer.User? { get { guard let userJson = userDefaults.data(forKey: KEY_USER) else { @@ -120,14 +118,14 @@ public class AppStorage: CoreStorage, ProfileStorage { } } } - + public func clear() { accessToken = nil refreshToken = nil cookiesDate = nil user = nil } - + private let KEY_ACCESS_TOKEN = "accessToken" private let KEY_REFRESH_TOKEN = "refreshToken" private let KEY_COOKIES_DATE = "cookiesDate" @@ -135,3 +133,10 @@ public class AppStorage: CoreStorage, ProfileStorage { private let KEY_USER = "refreshToken" private let KEY_SETTINGS = "userSettings" } + +// Mark - For testing and SwiftUI preview +#if DEBUG +public extension AppStorage { + static let mock: AppStorage = .init(keychain: KeychainSwift(), userDefaults: UserDefaults.standard) +} +#endif diff --git a/Core/Core/Data/CoreStorage.swift b/Core/Core/Data/CoreStorage.swift deleted file mode 100644 index 4ff71e963..000000000 --- a/Core/Core/Data/CoreStorage.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CoreStorage.swift -// Core -// -// Created by Vladimir Chekyrta on 13.09.2022. -// - -import Foundation - -public protocol CoreStorage { - var accessToken: String? {get set} - var refreshToken: String? {get set} - var cookiesDate: String? {get set} - var user: DataLayer.User? {get set} - var userSettings: UserSettings? {get set} - func clear() -} diff --git a/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents index 3dc8a8e03..79f74bbed 100644 --- a/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents +++ b/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/OpenEdX/Data/CorePersistence.swift b/Core/Core/Data/Persistence/CorePersistence.swift similarity index 76% rename from OpenEdX/Data/CorePersistence.swift rename to Core/Core/Data/Persistence/CorePersistence.swift index 75b136eb5..d465caabe 100644 --- a/OpenEdX/Data/CorePersistence.swift +++ b/Core/Core/Data/Persistence/CorePersistence.swift @@ -1,22 +1,37 @@ // // CorePersistence.swift -// OpenEdX +// Core // -// Created by  Stepanok Ivan on 25.07.2023. +// Created by  Stepanok Ivan on 08.03.2023. // -import Core -import Foundation import CoreData import Combine +public protocol CorePersistenceProtocol { + func publisher() -> AnyPublisher + func addToDownloadQueue(blocks: [CourseBlock]) + func getNextBlockForDownloading() -> DownloadData? + func getDownloadsForCourse(_ courseId: String) -> [DownloadData] + func downloadData(by blockId: String) -> DownloadData? + func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) + func deleteDownloadData(id: String) throws + func saveDownloadData(data: DownloadData) +} + public class CorePersistence: CorePersistenceProtocol { - private var context: NSManagedObjectContext + public init() {} - public init(context: NSManagedObjectContext) { - self.context = context - } + private let model = "CoreDataModel" + + private lazy var persistentContainer: NSPersistentContainer = { + return createContainer() + }() + + private lazy var context: NSManagedObjectContext = { + return createContext() + }() public func publisher() -> AnyPublisher { let notification = NSManagedObjectContext.didChangeObjectsNotification @@ -41,32 +56,13 @@ public class CorePersistence: CorePersistenceProtocol { .eraseToAnyPublisher() } - public func getAllDownloadData() -> [DownloadData] { - let request = CDDownloadData.fetchRequest() - guard let downloadData = try? context.fetch(request) else { return [] } - return downloadData.map { - DownloadData( - id: $0.id ?? "", - courseId: $0.courseId ?? "", - url: $0.url ?? "", - fileName: $0.fileName ?? "", - progress: $0.progress, - resumeData: $0.resumeData, - state: DownloadState(rawValue: $0.state ?? "") ?? .waiting, - type: DownloadType(rawValue: $0.type ?? "") ?? .video - ) - } - } - public func addToDownloadQueue(blocks: [CourseBlock]) { for block in blocks { let request = CDDownloadData.fetchRequest() request.predicate = NSPredicate(format: "id = %@", block.id) guard (try? context.fetch(request).first) == nil else { continue } guard let url = block.videoUrl, - let fileExtension = URL(string: url)?.pathExtension - else { continue } - let fileName = "\(block.id).\(fileExtension)" + let fileName = URL(string: url)?.lastPathComponent else { continue } context.performAndWait { let newDownloadData = CDDownloadData(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump @@ -182,4 +178,28 @@ public class CorePersistence: CorePersistenceProtocol { } } } + + private func createContainer() -> NSPersistentContainer { + let bundle = Bundle(for: Self.self) + let url = bundle.url(forResource: model, withExtension: "momd") + let managedObjectModel = NSManagedObjectModel(contentsOf: url!) + let container = NSPersistentContainer(name: model, managedObjectModel: managedObjectModel!) + container.loadPersistentStores(completionHandler: { (_, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + let description = NSPersistentStoreDescription() + description.shouldInferMappingModelAutomatically = true + description.shouldMigrateStoreAutomatically = true + container.persistentStoreDescriptions = [description] + + return container + } + + private func createContext() -> NSManagedObjectContext { + let context = persistentContainer.newBackgroundContext() + context.automaticallyMergesChangesFromParent = true + return context + } } diff --git a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift deleted file mode 100644 index f250fc49c..000000000 --- a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CorePersistence.swift -// Core -// -// Created by  Stepanok Ivan on 08.03.2023. -// - -import CoreData -import Combine - -public protocol CorePersistenceProtocol { - func publisher() -> AnyPublisher - func getAllDownloadData() -> [DownloadData] - func addToDownloadQueue(blocks: [CourseBlock]) - func getNextBlockForDownloading() -> DownloadData? - func getDownloadsForCourse(_ courseId: String) -> [DownloadData] - func downloadData(by blockId: String) -> DownloadData? - func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) - func deleteDownloadData(id: String) throws - func saveDownloadData(data: DownloadData) -} - -public final class CoreBundle { - private init() {} -} diff --git a/Core/Core/Data/Repository/AuthRepository.swift b/Core/Core/Data/Repository/AuthRepository.swift index 1ed62fd68..e945c8ea4 100644 --- a/Core/Core/Data/Repository/AuthRepository.swift +++ b/Core/Core/Data/Repository/AuthRepository.swift @@ -19,10 +19,10 @@ public protocol AuthRepositoryProtocol { public class AuthRepository: AuthRepositoryProtocol { private let api: API - private var appStorage: CoreStorage + private let appStorage: AppStorage private let config: Config - public init(api: API, appStorage: CoreStorage, config: Config) { + public init(api: API, appStorage: AppStorage, config: Config) { self.api = api self.appStorage = appStorage self.config = config diff --git a/Core/Core/Extensions/CGColorExtension.swift b/Core/Core/Extensions/CGColorExtension.swift index 2454e026d..3086aaf61 100644 --- a/Core/Core/Extensions/CGColorExtension.swift +++ b/Core/Core/Extensions/CGColorExtension.swift @@ -25,20 +25,3 @@ public extension CGColor { return hexString } } - -public extension Color { - func uiColor() -> UIColor { - let scanner = Scanner(string: description.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)) - var hexNumber: UInt64 = 0 - var r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0, a: CGFloat = 0.0 - - let result = scanner.scanHexInt64(&hexNumber) - if result { - r = CGFloat((hexNumber & 0xFF000000) >> 24) / 255 - g = CGFloat((hexNumber & 0x00FF0000) >> 16) / 255 - b = CGFloat((hexNumber & 0x0000FF00) >> 8) / 255 - a = CGFloat(hexNumber & 0x000000FF) / 255 - } - return UIColor(red: r, green: g, blue: b, alpha: a) - } -} diff --git a/Core/Core/Extensions/CollectionExtension.swift b/Core/Core/Extensions/CollectionExtension.swift index ba2ff088a..73fbeba15 100644 --- a/Core/Core/Extensions/CollectionExtension.swift +++ b/Core/Core/Extensions/CollectionExtension.swift @@ -7,7 +7,7 @@ import Foundation -public extension Collection { +extension Collection { /// Returns the element at the specified index if it is within bounds, otherwise nil. subscript (safe index: Index) -> Element? { return indices.contains(index) ? self[index] : nil diff --git a/Core/Core/Extensions/UIApplicationExtension.swift b/Core/Core/Extensions/UIApplicationExtension.swift index 616b9f466..1c5dec36c 100644 --- a/Core/Core/Extensions/UIApplicationExtension.swift +++ b/Core/Core/Extensions/UIApplicationExtension.swift @@ -34,30 +34,3 @@ extension UIApplication { return controller } } - -extension UINavigationController { - open override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - navigationBar.topItem?.backButtonDisplayMode = .minimal - navigationBar.barTintColor = .clear - navigationBar.setBackgroundImage(UIImage(), for: .default) - navigationBar.shadowImage = UIImage() - - let image = CoreAssets.arrowLeft.image - navigationBar.backIndicatorImage = image.withTintColor(CoreAssets.accentColor.color) - navigationBar.tintColor = .clear - navigationBar.backIndicatorTransitionMaskImage = image.withTintColor(CoreAssets.accentColor.color) - navigationBar.titleTextAttributes = [.foregroundColor: CoreAssets.textPrimary.color] - } -} - -extension UINavigationController: UIGestureRecognizerDelegate { - override open func viewDidLoad() { - super.viewDidLoad() - interactivePopGestureRecognizer?.delegate = self - } - - public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - return viewControllers.count > 1 - } -} diff --git a/Core/Core/Extensions/ViewExtension.swift b/Core/Core/Extensions/ViewExtension.swift index d6584cbcf..9dc0a8818 100644 --- a/Core/Core/Extensions/ViewExtension.swift +++ b/Core/Core/Extensions/ViewExtension.swift @@ -15,9 +15,9 @@ public extension View { top: CGFloat? = 0, bottom: CGFloat? = 0, leftLineEnabled: Bool = false, - bgColor: Color = Theme.Colors.background, - strokeColor: Color = Theme.Colors.cardViewStroke, - textColor: Color = Theme.Colors.textPrimary + bgColor: Color = CoreAssets.background.swiftUIColor, + strokeColor: Color = CoreAssets.cardViewStroke.swiftUIColor, + textColor: Color = CoreAssets.textPrimary.swiftUIColor ) -> some View { return self .padding(.all, 20) @@ -53,8 +53,8 @@ public extension View { func shadowCardStyle( top: CGFloat? = 0, bottom: CGFloat? = 0, - bgColor: Color = Theme.Colors.cardViewBackground, - textColor: Color = Theme.Colors.textPrimary + bgColor: Color = CoreAssets.cardViewBackground.swiftUIColor, + textColor: Color = CoreAssets.textPrimary.swiftUIColor ) -> some View { return self .padding(.all, 16) @@ -65,7 +65,7 @@ public extension View { alignment: .topLeading) .background(Theme.Shapes.cardShape .fill(bgColor) - .shadow(color: Theme.Colors.shadowColor, + .shadow(color: CoreAssets.shadowColor.swiftUIColor, radius: 12, y: 4)) .foregroundColor(textColor) .padding(.horizontal, 24) @@ -77,7 +77,7 @@ public extension View { func titleSettings( top: CGFloat? = 10, bottom: CGFloat? = 20, - color: Color = Theme.Colors.textPrimary + color: Color = CoreAssets.textPrimary.swiftUIColor ) -> some View { return self .lineLimit(1) @@ -98,8 +98,8 @@ public extension View { } func roundedBackground( - _ color: Color = Theme.Colors.background, - strokeColor: Color = Theme.Colors.backgroundStroke, + _ color: Color = CoreAssets.background.swiftUIColor, + strokeColor: Color = CoreAssets.backgroundStroke.swiftUIColor, ipadMaxHeight: CGFloat = .infinity, maxIpadWidth: CGFloat = 420 ) -> some View { @@ -166,6 +166,14 @@ public extension View { ) } + var isIOS14: Bool { + if #available(iOS 15.0, *) { + return false + } else { + return true + } + } + func onFirstAppear(_ action: @escaping () -> Void) -> some View { modifier(FirstAppear(action: action)) } @@ -188,13 +196,13 @@ private struct FirstAppear: ViewModifier { } public extension Image { - func backButtonStyle(topPadding: CGFloat = -10, color: Color = Theme.Colors.accentColor) -> some View { + func backButtonStyle(topPadding: CGFloat = -10, color: Color = CoreAssets.accentColor.swiftUIColor) -> some View { return self .renderingMode(.template) .resizable() .scaledToFit() .frame(height: 24) - .padding(.horizontal, 8) + .padding(.horizontal) .padding(.top, topPadding) .foregroundColor(color) } diff --git a/Core/Core/Network/DownloadManager.swift b/Core/Core/Network/DownloadManager.swift index 774c767f0..e635d1541 100644 --- a/Core/Core/Network/DownloadManager.swift +++ b/Core/Core/Network/DownloadManager.swift @@ -29,26 +29,6 @@ public struct DownloadData { public let resumeData: Data? public let state: DownloadState public let type: DownloadType - - public init( - id: String, - courseId: String, - url: String, - fileName: String, - progress: Double, - resumeData: Data?, - state: DownloadState, - type: DownloadType - ) { - self.id = id - self.courseId = courseId - self.url = url - self.fileName = fileName - self.progress = progress - self.resumeData = resumeData - self.state = state - self.type = type - } } public class NoWiFiError: LocalizedError { @@ -64,14 +44,13 @@ public protocol DownloadManagerProtocol { func resumeDownloading() throws func pauseDownloading() func deleteFile(blocks: [CourseBlock]) - func deleteAllFiles() func fileUrl(for blockId: String) -> URL? } public class DownloadManager: DownloadManagerProtocol { private let persistence: CorePersistenceProtocol - private let appStorage: CoreStorage + private let appStorage: Core.AppStorage private let connectivity: ConnectivityProtocol private var downloadRequest: DownloadRequest? private var currentDownload: DownloadData? @@ -79,7 +58,7 @@ public class DownloadManager: DownloadManagerProtocol { public init( persistence: CorePersistenceProtocol, - appStorage: CoreStorage, + appStorage: Core.AppStorage, connectivity: ConnectivityProtocol ) { self.persistence = persistence @@ -148,21 +127,20 @@ public class DownloadManager: DownloadManagerProtocol { resumeData: download.resumeData ) self.isDownloadingInProgress = true + let fileName = url.lastPathComponent if let resumeData = download.resumeData { downloadRequest = AF.download(resumingWith: resumeData) } else { downloadRequest = AF.download(url) } - #if DEBUG - downloadRequest?.downloadProgress { prog in - let completed = Double(prog.fractionCompleted * 100) - print(">>>>> Downloading", download.url, completed, "%") - } - #endif +// downloadRequest?.downloadProgress { prog in +// let completed = Double(prog.fractionCompleted * 100) +// print(">>>>> Downloading", download.url, completed, "%") +// } downloadRequest?.responseData(completionHandler: { [weak self] data in guard let self else { return } if let data = data.value, let url = self.videosFolderUrl() { - self.saveFile(fileName: download.fileName, data: data, folderURL: url) + self.saveFile(file: fileName, data: data, url: url) self.persistence.updateDownloadState( id: download.id, state: .finished, @@ -191,25 +169,15 @@ public class DownloadManager: DownloadManagerProtocol { public func deleteFile(blocks: [CourseBlock]) { for block in blocks { - do { - try persistence.deleteDownloadData(id: block.id) - if let fileUrl = fileUrl(for: block.id) { - try FileManager.default.removeItem(at: fileUrl) - } - } catch { - NSLog("Error deleting file: \(error.localizedDescription)") - } - } - } - - public func deleteAllFiles() { - let downloadData = persistence.getAllDownloadData() - downloadData.forEach { - if let fileURL = fileUrl(for: $0.id) { + if let url = block.videoUrl, + let fileName = URL(string: url)?.lastPathComponent, let folderUrl = videosFolderUrl() { do { - try FileManager.default.removeItem(at: fileURL) + let fileUrl = folderUrl.appendingPathComponent(fileName) + try persistence.deleteDownloadData(id: block.id) + try FileManager.default.removeItem(at: fileUrl) + print("File deleted successfully") } catch { - NSLog("Error deleting All files: \(error.localizedDescription)") + print("Error deleting file: \(error.localizedDescription)") } } } @@ -219,9 +187,10 @@ public class DownloadManager: DownloadManagerProtocol { guard let data = persistence.downloadData(by: blockId), data.url.count > 0, data.state == .finished else { return nil } - let path = videosFolderUrl() - let fileName = data.fileName - return path?.appendingPathComponent(fileName) + + let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + let directoryURL = documentDirectoryURL.appendingPathComponent("Files", isDirectory: true) + return directoryURL.appendingPathComponent(data.fileName) } private func videosFolderUrl() -> URL? { @@ -245,12 +214,12 @@ public class DownloadManager: DownloadManagerProtocol { } } - private func saveFile(fileName: String, data: Data, folderURL: URL) { - let fileURL = folderURL.appendingPathComponent(fileName) + private func saveFile(file: String, data: Data, url: URL) { + let fileURL = url.appendingPathComponent(file) do { try data.write(to: fileURL) } catch { - NSLog("SaveFile Error", error.localizedDescription) + print("SaveFile Error", error.localizedDescription) } } } @@ -291,10 +260,6 @@ public class DownloadManagerMock: DownloadManagerProtocol { } - public func deleteAllFiles() { - - } - public func fileUrl(for blockId: String) -> URL? { return nil } diff --git a/Core/Core/Network/RequestInterceptor.swift b/Core/Core/Network/RequestInterceptor.swift index cecdf0570..3f8e80b9a 100644 --- a/Core/Core/Network/RequestInterceptor.swift +++ b/Core/Core/Network/RequestInterceptor.swift @@ -11,11 +11,11 @@ import Alamofire final public class RequestInterceptor: Alamofire.RequestInterceptor { private let config: Config - private var storage: CoreStorage + private let appStorage: AppStorage - public init(config: Config, storage: CoreStorage) { + public init(config: Config, appStorage: AppStorage) { self.config = config - self.storage = storage + self.appStorage = appStorage } private let lock = NSLock() @@ -34,7 +34,7 @@ final public class RequestInterceptor: Alamofire.RequestInterceptor { var urlRequest = urlRequest // Set the Authorization header value using the access token. - if let token = storage.accessToken { + if let token = appStorage.accessToken { urlRequest.setValue("Bearer " + token, forHTTPHeaderField: "Authorization") } @@ -57,7 +57,7 @@ final public class RequestInterceptor: Alamofire.RequestInterceptor { return completion(.doNotRetry) } - guard let token = storage.refreshToken else { + guard let token = appStorage.refreshToken else { return completion(.doNotRetryWithError(error)) } @@ -117,8 +117,8 @@ final public class RequestInterceptor: Alamofire.RequestInterceptor { refreshToken.count > 0 else { return completion(false) } - self.storage.accessToken = accessToken - self.storage.refreshToken = refreshToken + self.appStorage.accessToken = accessToken + self.appStorage.refreshToken = refreshToken completion(true) } catch { completion(false) diff --git a/Core/Core/Theme.swift b/Core/Core/Theme.swift index b63ad99f9..56db63501 100644 --- a/Core/Core/Theme.swift +++ b/Core/Core/Theme.swift @@ -10,79 +10,6 @@ import SwiftUI public struct Theme { - public struct Colors { - public private(set) static var accentColor = CoreAssets.accentColor.swiftUIColor - public private(set) static var alert = CoreAssets.alert.swiftUIColor - public private(set) static var avatarStroke = CoreAssets.avatarStroke.swiftUIColor - public private(set) static var background = CoreAssets.background.swiftUIColor - public private(set) static var backgroundStroke = CoreAssets.backgroundStroke.swiftUIColor - public private(set) static var cardViewBackground = CoreAssets.cardViewBackground.swiftUIColor - public private(set) static var cardViewStroke = CoreAssets.cardViewStroke.swiftUIColor - public private(set) static var certificateForeground = CoreAssets.certificateForeground.swiftUIColor - public private(set) static var commentCellBackground = CoreAssets.commentCellBackground.swiftUIColor - public private(set) static var shadowColor = CoreAssets.shadowColor.swiftUIColor - public private(set) static var snackbarErrorColor = CoreAssets.snackbarErrorColor.swiftUIColor - public private(set) static var snackbarErrorTextColor = CoreAssets.snackbarErrorTextColor.swiftUIColor - public private(set) static var snackbarInfoAlert = CoreAssets.snackbarInfoAlert.swiftUIColor - public private(set) static var styledButtonBackground = CoreAssets.styledButtonBackground.swiftUIColor - public private(set) static var styledButtonText = CoreAssets.styledButtonText.swiftUIColor - public private(set) static var textPrimary = CoreAssets.textPrimary.swiftUIColor - public private(set) static var textSecondary = CoreAssets.textSecondary.swiftUIColor - public private(set) static var textInputBackground = CoreAssets.textInputBackground.swiftUIColor - public private(set) static var textInputStroke = CoreAssets.textInputStroke.swiftUIColor - public private(set) static var textInputUnfocusedBackground = CoreAssets.textInputUnfocusedBackground.swiftUIColor - public private(set) static var textInputUnfocusedStroke = CoreAssets.textInputUnfocusedStroke.swiftUIColor - public private(set) static var warning = CoreAssets.warning.swiftUIColor - - public static func update( - accentColor: Color = CoreAssets.accentColor.swiftUIColor, - alert: Color = CoreAssets.alert.swiftUIColor, - avatarStroke: Color = CoreAssets.avatarStroke.swiftUIColor, - background: Color = CoreAssets.background.swiftUIColor, - backgroundStroke: Color = CoreAssets.backgroundStroke.swiftUIColor, - cardViewBackground: Color = CoreAssets.cardViewBackground.swiftUIColor, - cardViewStroke: Color = CoreAssets.cardViewStroke.swiftUIColor, - certificateForeground: Color = CoreAssets.certificateForeground.swiftUIColor, - commentCellBackground: Color = CoreAssets.commentCellBackground.swiftUIColor, - shadowColor: Color = CoreAssets.shadowColor.swiftUIColor, - snackbarErrorColor: Color = CoreAssets.snackbarErrorColor.swiftUIColor, - snackbarErrorTextColor: Color = CoreAssets.snackbarErrorTextColor.swiftUIColor, - snackbarInfoAlert: Color = CoreAssets.snackbarInfoAlert.swiftUIColor, - styledButtonBackground: Color = CoreAssets.styledButtonBackground.swiftUIColor, - styledButtonText: Color = CoreAssets.styledButtonText.swiftUIColor, - textPrimary: Color = CoreAssets.textPrimary.swiftUIColor, - textSecondary: Color = CoreAssets.textSecondary.swiftUIColor, - textInputBackground: Color = CoreAssets.textInputBackground.swiftUIColor, - textInputStroke: Color = CoreAssets.textInputStroke.swiftUIColor, - textInputUnfocusedBackground: Color = CoreAssets.textInputUnfocusedBackground.swiftUIColor, - textInputUnfocusedStroke: Color = CoreAssets.textInputUnfocusedStroke.swiftUIColor, - warning: Color = CoreAssets.warning.swiftUIColor - ) { - self.accentColor = accentColor - self.alert = alert - self.avatarStroke = avatarStroke - self.background = background - self.backgroundStroke = backgroundStroke - self.cardViewBackground = cardViewBackground - self.cardViewStroke = cardViewStroke - self.certificateForeground = certificateForeground - self.commentCellBackground = commentCellBackground - self.shadowColor = shadowColor - self.snackbarErrorColor = snackbarErrorColor - self.snackbarErrorTextColor = snackbarErrorTextColor - self.snackbarInfoAlert = snackbarInfoAlert - self.styledButtonBackground = styledButtonBackground - self.styledButtonText = styledButtonText - self.textPrimary = textPrimary - self.textSecondary = textSecondary - self.textInputBackground = textInputBackground - self.textInputStroke = textInputStroke - self.textInputUnfocusedBackground = textInputUnfocusedBackground - self.textInputUnfocusedStroke = textInputUnfocusedStroke - self.warning = warning - } - } - public struct Fonts { public static let displayLarge: Font = .custom("SFPro-Regular", size: 57) diff --git a/Core/Core/View/Base/AlertView.swift b/Core/Core/View/Base/AlertView.swift index f230eba9b..e4dd061a2 100644 --- a/Core/Core/View/Base/AlertView.swift +++ b/Core/Core/View/Base/AlertView.swift @@ -135,7 +135,7 @@ public struct AlertView: View { .padding(.horizontal, 40) .multilineTextAlignment(.center) .font(Theme.Fonts.labelSmall) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) } } @@ -157,7 +157,7 @@ public struct AlertView: View { }) .background( Theme.Shapes.buttonShape - .fill(Theme.Colors.warning) + .fill(CoreAssets.warning.swiftUIColor) ) .overlay( RoundedRectangle(cornerRadius: 8) @@ -186,7 +186,7 @@ public struct AlertView: View { }) .background( Theme.Shapes.buttonShape - .fill(Theme.Colors.warning) + .fill(CoreAssets.warning.swiftUIColor) ) .overlay( RoundedRectangle(cornerRadius: 8) @@ -205,7 +205,7 @@ public struct AlertView: View { }, label: { ZStack { Text(CoreLocalization.Alert.keepEditing) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .font(Theme.Fonts.labelLarge) .frame(maxWidth: .infinity) .padding(.horizontal, 16) @@ -224,7 +224,7 @@ public struct AlertView: View { lineJoin: .round, miterLimit: 1 )) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) ) .frame(maxWidth: 215) } @@ -235,7 +235,7 @@ public struct AlertView: View { } .background( Theme.Shapes.cardShape - .fill(Theme.Colors.cardViewBackground) + .fill(CoreAssets.cardViewBackground.swiftUIColor) .shadow(radius: 24) .frame(width: reader.size.width < 420 ? reader.size.width - 80 @@ -244,7 +244,7 @@ public struct AlertView: View { .overlay( RoundedRectangle(cornerRadius: 12) .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) - .foregroundColor(Theme.Colors.backgroundStroke) + .foregroundColor(CoreAssets.backgroundStroke.swiftUIColor) .frame(width: reader.size.width < 420 ? reader.size.width - 80 : 360) diff --git a/Core/Core/View/Base/CourseButton.swift b/Core/Core/View/Base/CourseButton.swift index 4d9a468d9..a7473e36e 100644 --- a/Core/Core/View/Base/CourseButton.swift +++ b/Core/Core/View/Base/CourseButton.swift @@ -31,12 +31,12 @@ public struct CourseButton: View { .foregroundColor(.accentColor) } else { image - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) } Text(displayName) .font(Theme.Fonts.titleMedium) .multilineTextAlignment(.leading) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() Image(systemName: "chevron.right") .padding(.vertical, 8) diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift index ae21b2e7f..cc5e83022 100644 --- a/Core/Core/View/Base/CourseCellView.swift +++ b/Core/Core/View/Base/CourseCellView.swift @@ -52,12 +52,12 @@ public struct CourseCellView: View { VStack(alignment: .leading) { Text(courseOrg) .font(Theme.Fonts.labelMedium) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) .multilineTextAlignment(.leading) Text(courseName) .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .lineLimit(type == .discovery ? 3 : 2) .multilineTextAlignment(.leading) .padding(.top, 1) @@ -67,18 +67,18 @@ public struct CourseCellView: View { if courseEnd != "" { Text(courseEnd) .font(Theme.Fonts.labelMedium) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) } else { Text(courseStart) .font(Theme.Fonts.labelMedium) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) } Spacer() CoreAssets.arrowRight16.swiftUIImage.renderingMode(.template) .resizable() .frame(width: 16, height: 16) .offset(x: 15) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) } } }.padding(.horizontal, 10) @@ -87,7 +87,7 @@ public struct CourseCellView: View { } }.frame(height: 105) - .background(Theme.Colors.background) + .background(CoreAssets.background.swiftUIColor) .opacity(showView ? 1 : 0) .offset(y: showView ? 0 : 20) .onAppear { @@ -102,7 +102,7 @@ public struct CourseCellView: View { if Int(index) != cellsCount { Divider() .frame(height: 1) - .overlay(Theme.Colors.cardViewStroke) + .overlay(CoreAssets.cardViewStroke.swiftUIColor) .padding(.vertical, 18) .padding(.horizontal, 3) } diff --git a/Core/Core/View/Base/DownloadView.swift b/Core/Core/View/Base/DownloadView.swift index 9956cc947..aa9b1187f 100644 --- a/Core/Core/View/Base/DownloadView.swift +++ b/Core/Core/View/Base/DownloadView.swift @@ -22,7 +22,7 @@ public struct DownloadAvailableView: View { .resizable() .scaledToFit() .frame(width: 24, height: 24) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) } } @@ -37,7 +37,7 @@ public struct DownloadProgressView: View { .resizable() .scaledToFit() .frame(width: 20, height: 20) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(6) } } @@ -52,6 +52,6 @@ public struct DownloadFinishedView: View { .resizable() .scaledToFit() .frame(width: 24, height: 24) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) } } diff --git a/Core/Core/View/Base/FlexibleKeyboardInputView.swift b/Core/Core/View/Base/FlexibleKeyboardInputView.swift index 97bc11401..83360f42c 100644 --- a/Core/Core/View/Base/FlexibleKeyboardInputView.swift +++ b/Core/Core/View/Base/FlexibleKeyboardInputView.swift @@ -46,15 +46,15 @@ public struct FlexibleKeyboardInputView: View { .overlay( TextEditor(text: $commentText) .padding(.horizontal, 8) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .hideScrollContentBackground() .frame(maxHeight: commentSize) .background( ZStack(alignment: .leading) { Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) Text(commentText.count == 0 ? hint : "") - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) .font(Theme.Fonts.labelLarge) .padding(.leading, 14) } @@ -63,7 +63,7 @@ public struct FlexibleKeyboardInputView: View { Theme.Shapes.textInputShape .stroke(lineWidth: 1) .fill( - Theme.Colors.textInputStroke + CoreAssets.textInputStroke.swiftUIColor ) ) ).padding(8) @@ -87,14 +87,14 @@ public struct FlexibleKeyboardInputView: View { .padding(.trailing, 14) }.frame(maxWidth: .infinity, maxHeight: commentSize + 16) .background( - Theme.Colors.commentCellBackground + CoreAssets.commentCellBackground.swiftUIColor .ignoresSafeArea() ) .overlay( GeometryReader { proxy in Rectangle() .size(width: proxy.size.width, height: 1) - .foregroundColor(Theme.Colors.cardViewStroke) + .foregroundColor(CoreAssets.cardViewStroke.swiftUIColor) } ) } diff --git a/Core/Core/View/Base/NavigationBar.swift b/Core/Core/View/Base/NavigationBar.swift index 2af6581f9..2b589d030 100644 --- a/Core/Core/View/Base/NavigationBar.swift +++ b/Core/Core/View/Base/NavigationBar.swift @@ -24,8 +24,8 @@ public struct NavigationBar: View { @Binding private var rightButtonIsActive: Bool public init(title: String, - titleColor: Color = Theme.Colors.textPrimary, - leftButtonColor: Color = Theme.Colors.accentColor, + titleColor: Color = CoreAssets.textPrimary.swiftUIColor, + leftButtonColor: Color = CoreAssets.accentColor.swiftUIColor, leftButtonAction: (() -> Void)? = nil, rightButtonType: ButtonType? = nil, rightButtonAction: (() -> Void)? = nil, @@ -56,7 +56,7 @@ public struct NavigationBar: View { CoreAssets.arrowLeft.swiftUIImage .backButtonStyle(color: leftButtonColor) }) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) }.frame(minWidth: 0, maxWidth: .infinity, @@ -76,7 +76,7 @@ public struct NavigationBar: View { .backButtonStyle(topPadding: 0) Text(CoreLocalization.done) .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) }.offset(y: -6) case .edit: CoreAssets.edit.swiftUIImage @@ -91,12 +91,12 @@ public struct NavigationBar: View { }) .opacity(rightButtonIsActive ? 1 : 0.3) .padding(.trailing, 16) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) }.frame(minWidth: 0, maxWidth: .infinity, alignment: .topTrailing) } - } + } .frameLimit() } } diff --git a/Core/Core/View/Base/OfflineSnackBarView.swift b/Core/Core/View/Base/OfflineSnackBarView.swift index bc7a01279..153a96bcb 100644 --- a/Core/Core/View/Base/OfflineSnackBarView.swift +++ b/Core/Core/View/Base/OfflineSnackBarView.swift @@ -46,7 +46,7 @@ public struct OfflineSnackBarView: View { }.padding(.horizontal, 16) .font(Theme.Fonts.titleSmall) .frame(maxWidth: .infinity, maxHeight: OfflineSnackBarView.height) - .background(Theme.Colors.warning.ignoresSafeArea()) + .background(CoreAssets.warning.swiftUIColor.ignoresSafeArea()) } } .onAppear { diff --git a/Core/Core/View/Base/PickerMenu.swift b/Core/Core/View/Base/PickerMenu.swift index a967ffdde..14da74cdd 100644 --- a/Core/Core/View/Base/PickerMenu.swift +++ b/Core/Core/View/Base/PickerMenu.swift @@ -78,21 +78,21 @@ public struct PickerMenu: View { Spacer() VStack { Text(titleText) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) TextField(CoreLocalization.Picker.search, text: $search) .padding(.all, 8) - .background(Theme.Colors.textInputStroke.cornerRadius(6)) + .background(CoreAssets.textInputStroke.swiftUIColor.cornerRadius(6)) Picker("", selection: $selectedItem) { ForEach(filteredItems, id: \.self) { item in Text(item.value) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) } } .pickerStyle(.wheel) } .frame(minWidth: 0, maxWidth: idiom == .pad ? ipadPickerWidth : .infinity) .padding() - .background(Theme.Colors.textInputBackground.cornerRadius(16)) + .background(CoreAssets.textInputBackground.swiftUIColor.cornerRadius(16)) .padding(.horizontal, 16) .onChange(of: search, perform: { _ in if let first = filteredItems.first { @@ -105,10 +105,10 @@ public struct PickerMenu: View { router.dismiss(animated: true) }) { Text(CoreLocalization.Picker.accept) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .frame(minWidth: 0, maxWidth: idiom == .pad ? ipadPickerWidth : .infinity) .padding() - .background(Theme.Colors.textInputBackground.cornerRadius(16)) + .background(CoreAssets.textInputBackground.swiftUIColor.cornerRadius(16)) .padding(.horizontal, 16) } .padding(.bottom, 4) diff --git a/Core/Core/View/Base/PickerView.swift b/Core/Core/View/Base/PickerView.swift index fe8f10adc..d19d73116 100644 --- a/Core/Core/View/Base/PickerView.swift +++ b/Core/Core/View/Base/PickerView.swift @@ -23,7 +23,7 @@ public struct PickerView: View { Group { Text(config.field.label) .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.top, 18) HStack { Button(action: { @@ -48,16 +48,16 @@ public struct PickerView: View { Image(systemName: "chevron.down") }) }.padding(.all, 14) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) .fill(config.error == "" ? - Theme.Colors.textInputStroke + CoreAssets.textInputStroke.swiftUIColor : Color.red) ) .shake($config.shake) @@ -65,7 +65,7 @@ public struct PickerView: View { : config.error) .font(Theme.Fonts.labelMedium) .foregroundColor(config.error == "" - ? Theme.Colors.textPrimary + ? CoreAssets.textPrimary.swiftUIColor : Color.red) } } diff --git a/Core/Core/View/Base/ProgressBar.swift b/Core/Core/View/Base/ProgressBar.swift index 9e75e7985..7091194f8 100644 --- a/Core/Core/View/Base/ProgressBar.swift +++ b/Core/Core/View/Base/ProgressBar.swift @@ -22,9 +22,9 @@ public struct ProgressBar: View { private let gradient = AngularGradient( gradient: Gradient(colors: [ - Theme.Colors.accentColor.opacity(0.7), - Theme.Colors.accentColor.opacity(0.35), - Theme.Colors.accentColor.opacity(0.01)]), + CoreAssets.accentColor.swiftUIColor.opacity(0.7), + CoreAssets.accentColor.swiftUIColor.opacity(0.35), + CoreAssets.accentColor.swiftUIColor.opacity(0.01)]), center: .center, startAngle: .degrees(270), endAngle: .degrees(0)) diff --git a/Core/Core/View/Base/RefreshableScrollView.swift b/Core/Core/View/Base/RefreshableScrollView.swift index 0905bdba6..3a942d130 100644 --- a/Core/Core/View/Base/RefreshableScrollView.swift +++ b/Core/Core/View/Base/RefreshableScrollView.swift @@ -317,6 +317,87 @@ public extension View { } } +public struct RefreshableScrollViewIOS14: View { + public let onRefresh: OnRefresh // the refreshing action + public let content: Content // the ScrollView content + private let THRESHOLD: CGFloat = 100 + + @State private var state = RefreshState.waiting // the current state + + // We use a custom constructor to allow for usage of a @ViewBuilder for the content + public init(onRefresh: @escaping OnRefresh, @ViewBuilder content: () -> Content) { + self.onRefresh = onRefresh + self.content = content() + } + + public var body: some View { + // The root view is a regular ScrollView + ScrollView { + // The ZStack allows us to position the PositionIndicator, + // the content and the loading view, all on top of each other. + ZStack(alignment: .top) { + // The moving positioning indicator, that sits at the top + // of the ScrollView and scrolls down with the content + PositionIndicator(type: .moving) + .frame(height: 0) + + // Your ScrollView content. If we're loading, we want + // to keep it below the loading view, hence the alignmentGuide. + content + .alignmentGuide(.top, computeValue: { _ in + (state == .loading) ? -THRESHOLD : 0 + }) + + // The loading view. It's offset to the top of the content unless we're loading. + ZStack { + Rectangle() + .foregroundColor(.clear) + .frame(height: THRESHOLD) + + ActivityIndicator(isAnimating: state == .loading) { + $0.hidesWhenStopped = false + } + }.offset(y: (state == .loading) ? 0 : -THRESHOLD) + } + } + // Put a fixed PositionIndicator in the background so that we have + // a reference point to compute the scroll offset. + .background(PositionIndicator(type: .fixed)) + // Once the scrolling offset changes, we want to see if there should + // be a state change. + .onPreferenceChange(PositionPreferenceKey.self) { values in + if state != .loading { // If we're already loading, ignore everything + // Map the preference change action to the UI thread + DispatchQueue.main.async { + // Compute the offset between the moving and fixed PositionIndicators + let movingY = values.first { $0.type == .moving }?.y ?? 0 + let fixedY = values.first { $0.type == .fixed }?.y ?? 0 + let offset = movingY - fixedY + + // If the user pulled down below the threshold, prime the view + if offset > THRESHOLD && state == .waiting { + state = .primed + + // If the view is primed and we've crossed the threshold again on the + // way back, trigger the refresh + } else if offset < THRESHOLD && state == .primed { + state = .loading + self.state = .waiting + + onRefresh { // trigger the refreshing callback + // once refreshing is done, smoothly move the loading view + // back to the offset position + // withAnimation { + // self.state = .waiting + // } + } + } + } + } + } + } +} + struct ActivityIndicator: UIViewRepresentable { public typealias UIView = UIActivityIndicatorView public var isAnimating: Bool = true diff --git a/Core/Core/View/Base/RefreshableScrollViewCompat.swift b/Core/Core/View/Base/RefreshableScrollViewCompat.swift index 768aa08b9..446e472be 100644 --- a/Core/Core/View/Base/RefreshableScrollViewCompat.swift +++ b/Core/Core/View/Base/RefreshableScrollViewCompat.swift @@ -2,7 +2,7 @@ // RefreshableScrollViewCompat.swift // Core // -// Created by  Stepanok Ivan on 15.09.2023. +// Created by  Stepanok Ivan on 15.02.2023. // import SwiftUI @@ -17,16 +17,17 @@ public struct RefreshableScrollViewCompat: View where Content: View { } public var body: some View { - if #available(iOS 16.0, *) { - return ScrollView { - content() - }.refreshable { + if #available(iOS 15.0, *) { + return RefreshableScrollView(onRefresh: { done in Task { await action() + done() } + }) { + content() } } else { - return RefreshableScrollView(onRefresh: { done in + return RefreshableScrollViewIOS14(onRefresh: { done in Task { await action() done() diff --git a/Core/Core/View/Base/RegistrationTextField.swift b/Core/Core/View/Base/RegistrationTextField.swift index 1d68e8022..8fa28a7a6 100644 --- a/Core/Core/View/Base/RegistrationTextField.swift +++ b/Core/Core/View/Base/RegistrationTextField.swift @@ -35,7 +35,7 @@ public struct RegistrationTextField: View { if config.field.label != "" { Text(config.field.label) .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.top, 18) } if isTextArea { @@ -46,7 +46,7 @@ public struct RegistrationTextField: View { .hideScrollContentBackground() .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) ) .overlay( @@ -54,7 +54,7 @@ public struct RegistrationTextField: View { .stroke(lineWidth: 1) .fill( config.error == "" ? - Theme.Colors.textInputStroke + CoreAssets.textInputStroke.swiftUIColor : Color.red ) ) @@ -69,14 +69,14 @@ public struct RegistrationTextField: View { .padding(.all, 14) .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) .fill( config.error == "" ? - Theme.Colors.textInputStroke + CoreAssets.textInputStroke.swiftUIColor : Color.red ) ) @@ -90,14 +90,14 @@ public struct RegistrationTextField: View { .padding(.all, 14) .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) .fill( config.error == "" ? - Theme.Colors.textInputStroke + CoreAssets.textInputStroke.swiftUIColor : Color.red ) ) @@ -108,7 +108,7 @@ public struct RegistrationTextField: View { Text(config.error == "" ? config.field.instructions : config.error) .font(Theme.Fonts.bodySmall) .foregroundColor(config.error == "" - ? Theme.Colors.textSecondary + ? CoreAssets.textSecondary.swiftUIColor : Color.red) } } diff --git a/Core/Core/View/Base/SnackBarView.swift b/Core/Core/View/Base/SnackBarView.swift index 0272c15ba..0469124d4 100644 --- a/Core/Core/View/Base/SnackBarView.swift +++ b/Core/Core/View/Base/SnackBarView.swift @@ -36,7 +36,7 @@ public struct SnackBarView: View { .font(Theme.Fonts.titleSmall) } - }.shadowCardStyle(bgColor: Theme.Colors.snackbarErrorColor, + }.shadowCardStyle(bgColor: CoreAssets.snackbarErrorColor.swiftUIColor, textColor: .white) .padding(.bottom, 10) } diff --git a/Core/Core/View/Base/StyledButton.swift b/Core/Core/View/Base/StyledButton.swift index b16f61a1a..65d223447 100644 --- a/Core/Core/View/Base/StyledButton.swift +++ b/Core/Core/View/Base/StyledButton.swift @@ -15,24 +15,23 @@ public struct StyledButton: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } private let buttonColor: Color private let textColor: Color - private let isActive: Bool public init(_ title: String, action: @escaping () -> Void, isTransparent: Bool = false, - color: Color = Theme.Colors.accentColor, + color: Color = CoreAssets.accentColor.swiftUIColor, isActive: Bool = true) { self.title = title self.action = action self.isTransparent = isTransparent if isActive { self.buttonColor = color - self.textColor = Theme.Colors.styledButtonText + self.textColor = CoreAssets.styledButtonText.swiftUIColor } else { - self.buttonColor = Theme.Colors.cardViewStroke - self.textColor = Theme.Colors.textPrimary + self.buttonColor = CoreAssets.cardViewStroke.swiftUIColor + self.textColor = CoreAssets.textPrimary.swiftUIColor + } - self.isActive = isActive } public var body: some View { @@ -44,7 +43,6 @@ public struct StyledButton: View { .frame(maxWidth: .infinity) .padding(.horizontal, 16) } - .disabled(!isActive) .frame(maxWidth: idiom == .pad ? 260: .infinity, minHeight: isTransparent ? 36 : 42) .background( Theme.Shapes.buttonShape diff --git a/Core/Core/View/Base/UnitButtonView.swift b/Core/Core/View/Base/UnitButtonView.swift index 67a49d0da..52b3900c9 100644 --- a/Core/Core/View/Base/UnitButtonView.swift +++ b/Core/Core/View/Base/UnitButtonView.swift @@ -63,71 +63,71 @@ public struct UnitButtonView: View { case .first: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) .font(Theme.Fonts.labelLarge) CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) .rotationEffect(Angle.degrees(-90)) }.padding(.horizontal, 16) case .next, .nextBig: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) .padding(.leading, 20) .font(Theme.Fonts.labelLarge) if type != .nextBig { Spacer() } CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) .rotationEffect(Angle.degrees(-90)) .padding(.trailing, 20) } case .previous: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) .font(Theme.Fonts.labelLarge) .padding(.leading, 20) CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) .rotationEffect(Angle.degrees(90)) .padding(.trailing, 20) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) } case .last: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) .padding(.leading, 16) .font(Theme.Fonts.labelLarge) Spacer() CoreAssets.check.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) .padding(.trailing, 16) } case .finish: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) .font(Theme.Fonts.labelLarge) CoreAssets.check.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) }.padding(.horizontal, 16) case .reload, .custom: VStack(alignment: .center) { Text(type.stringValue()) - .foregroundColor(bgColor == nil ? .white : Theme.Colors.accentColor) + .foregroundColor(bgColor == nil ? .white : CoreAssets.accentColor.swiftUIColor) .font(Theme.Fonts.labelLarge) }.padding(.horizontal, 16) case .continueLesson, .nextSection: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) .padding(.leading, 20) .font(Theme.Fonts.labelLarge) CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) .rotationEffect(Angle.degrees(180)) .padding(.trailing, 20) } @@ -140,8 +140,8 @@ public struct UnitButtonView: View { case .first, .next, .nextBig, .previous, .last: Theme.Shapes.buttonShape .fill(type == .previous - ? Theme.Colors.background - : Theme.Colors.accentColor) + ? CoreAssets.background.swiftUIColor + : CoreAssets.accentColor.swiftUIColor) .shadow(color: Color.black.opacity(0.25), radius: 21, y: 4) .overlay( RoundedRectangle(cornerRadius: 8) @@ -151,12 +151,12 @@ public struct UnitButtonView: View { lineJoin: .round, miterLimit: 1) ) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) ) case .continueLesson, .nextSection, .reload, .finish, .custom: Theme.Shapes.buttonShape - .fill(bgColor ?? Theme.Colors.accentColor) + .fill(bgColor ?? CoreAssets.accentColor.swiftUIColor) .shadow(color: (type == .first || type == .next @@ -173,7 +173,7 @@ public struct UnitButtonView: View { lineJoin: .round, miterLimit: 1 )) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) ) } } diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift index ffea4335f..6d89e4528 100644 --- a/Core/Core/View/Base/WebBrowser.swift +++ b/Core/Core/View/Base/WebBrowser.swift @@ -22,26 +22,28 @@ public struct WebBrowser: View { public var body: some View { ZStack(alignment: .top) { - CoreAssets.background.swiftUIColor.ignoresSafeArea() + // 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 { + NavigationView { WebView( viewModel: .init(url: url, baseURL: ""), isLoading: $isShowProgress, refreshCookies: {} ) - -// } - }.navigationBarTitle(Text("")) // Needed for hide navBar on ios 14, 15 - .navigationBarHidden(true) - .ignoresSafeArea() + .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 ce8a9e098..7b4ed8157 100644 --- a/Core/Core/View/Base/WebUnitView.swift +++ b/Core/Core/View/Base/WebUnitView.swift @@ -28,9 +28,9 @@ public struct WebUnitView: View { .resizable() .scaledToFit() .frame(width: 64) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Text(viewModel.errorMessage ?? "") - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .multilineTextAlignment(.center) .padding(.horizontal, 20) Button(action: { @@ -43,7 +43,7 @@ public struct WebUnitView: View { .background(Theme.Shapes.buttonShape.fill(.clear)) .overlay(RoundedRectangle(cornerRadius: 8) .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) ) }) .frame(width: 100) diff --git a/Core/Core/View/Base/WebView.swift b/Core/Core/View/Base/WebView.swift index 9534f7529..463a9a315 100644 --- a/Core/Core/View/Base/WebView.swift +++ b/Core/Core/View/Base/WebView.swift @@ -75,19 +75,14 @@ public struct WebView: UIViewRepresentable { _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction ) async -> WKNavigationActionPolicy { - - guard let url = navigationAction.request.url else { return .cancel } + guard let url = navigationAction.request.url else { + return .cancel + } let baseURL = await parent.viewModel.baseURL if !baseURL.isEmpty, !url.absoluteString.starts(with: baseURL) { - if navigationAction.navigationType == .other { - return .allow - } else if navigationAction.navigationType == .linkActivated { - await MainActor.run { - UIApplication.shared.open(url, options: [:]) - } - } else if navigationAction.navigationType == .formSubmitted { - return .allow + await MainActor.run { + UIApplication.shared.open(url, options: [:]) } return .cancel } diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index c80d63032..9baaaedb5 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5C294B4FDA0032823A /* CourseCoreModel.xcdatamodeld */; }; - 02280F60294B50030032823A /* CoursePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */; }; + 02280F60294B50030032823A /* CoursePersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5F294B50030032823A /* CoursePersistence.swift */; }; 022C64D829ACEC48000F532B /* HandoutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64D729ACEC48000F532B /* HandoutsView.swift */; }; 022C64DA29ACEC50000F532B /* HandoutsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64D929ACEC50000F532B /* HandoutsViewModel.swift */; }; 022C64DC29ACFDEE000F532B /* Data_HandoutsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64DB29ACFDEE000F532B /* Data_HandoutsResponse.swift */; }; @@ -44,7 +44,7 @@ 02B6B3B228E1C49400232911 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 02B6B3B428E1C49400232911 /* Localizable.strings */; }; 02B6B3B728E1D11E00232911 /* CourseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3B628E1D11E00232911 /* CourseInteractor.swift */; }; 02B6B3BC28E1D14F00232911 /* CourseRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3BB28E1D14F00232911 /* CourseRepository.swift */; }; - 02B6B3BE28E1D15C00232911 /* CourseEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3BD28E1D15C00232911 /* CourseEndpoint.swift */; }; + 02B6B3BE28E1D15C00232911 /* CourseDetailsEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3BD28E1D15C00232911 /* CourseDetailsEndpoint.swift */; }; 02B6B3C128E1DBA100232911 /* Data_CourseDetailsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3C028E1DBA100232911 /* Data_CourseDetailsResponse.swift */; }; 02B6B3C328E1DCD100232911 /* CourseDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3C228E1DCD100232911 /* CourseDetails.swift */; }; 02B6B3C928E1E68100232911 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02B6B3C828E1E68100232911 /* Core.framework */; }; @@ -78,7 +78,7 @@ /* Begin PBXFileReference section */ 02280F5D294B4FDA0032823A /* CourseCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CourseCoreModel.xcdatamodel; sourceTree = ""; }; - 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursePersistenceProtocol.swift; sourceTree = ""; }; + 02280F5F294B50030032823A /* CoursePersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursePersistence.swift; sourceTree = ""; }; 022C64D729ACEC48000F532B /* HandoutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoutsView.swift; sourceTree = ""; }; 022C64D929ACEC50000F532B /* HandoutsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoutsViewModel.swift; sourceTree = ""; }; 022C64DB29ACFDEE000F532B /* Data_HandoutsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_HandoutsResponse.swift; sourceTree = ""; }; @@ -115,7 +115,7 @@ 02B6B3B328E1C49400232911 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 02B6B3B628E1D11E00232911 /* CourseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInteractor.swift; sourceTree = ""; }; 02B6B3BB28E1D14F00232911 /* CourseRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseRepository.swift; sourceTree = ""; }; - 02B6B3BD28E1D15C00232911 /* CourseEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseEndpoint.swift; sourceTree = ""; }; + 02B6B3BD28E1D15C00232911 /* CourseDetailsEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDetailsEndpoint.swift; sourceTree = ""; }; 02B6B3C028E1DBA100232911 /* Data_CourseDetailsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseDetailsResponse.swift; sourceTree = ""; }; 02B6B3C228E1DCD100232911 /* CourseDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDetails.swift; sourceTree = ""; }; 02B6B3C828E1E68100232911 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -186,7 +186,7 @@ 0208666A29CC6D8000BC05B2 /* Persistence */ = { isa = PBXGroup; children = ( - 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */, + 02280F5F294B50030032823A /* CoursePersistence.swift */, 02280F5C294B4FDA0032823A /* CourseCoreModel.xcdatamodeld */, ); path = Persistence; @@ -278,7 +278,7 @@ 02B6B3B928E1D13500232911 /* Network */ = { isa = PBXGroup; children = ( - 02B6B3BD28E1D15C00232911 /* CourseEndpoint.swift */, + 02B6B3BD28E1D15C00232911 /* CourseDetailsEndpoint.swift */, ); path = Network; sourceTree = ""; @@ -683,7 +683,7 @@ 02454CA02A2618E70043052A /* YouTubeView.swift in Sources */, 02454CA22A26190A0043052A /* EncodedVideoView.swift in Sources */, 02B6B3BC28E1D14F00232911 /* CourseRepository.swift in Sources */, - 02280F60294B50030032823A /* CoursePersistenceProtocol.swift in Sources */, + 02280F60294B50030032823A /* CoursePersistence.swift in Sources */, 02454CAA2A2619B40043052A /* LessonProgressView.swift in Sources */, 02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */, 0766DFCE299AB26D00EBEF6A /* EncodedVideoPlayer.swift in Sources */, @@ -715,7 +715,7 @@ 022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */, 02E685BE28E4B60A000AE015 /* CourseDetailsView.swift in Sources */, 02F175372A4DAFD20019CD70 /* CourseAnalytics.swift in Sources */, - 02B6B3BE28E1D15C00232911 /* CourseEndpoint.swift in Sources */, + 02B6B3BE28E1D15C00232911 /* CourseDetailsEndpoint.swift in Sources */, 02B6B3C328E1DCD100232911 /* CourseDetails.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -752,7 +752,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -773,7 +773,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -794,7 +794,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -815,7 +815,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -836,7 +836,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -857,7 +857,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1007,7 +1007,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1042,7 +1042,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1140,7 +1140,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1239,7 +1239,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1332,7 +1332,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1424,7 +1424,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1522,7 +1522,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1550,7 +1550,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1636,7 +1636,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1663,7 +1663,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index 68e833255..6be4d39e9 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -24,12 +24,12 @@ public protocol CourseRepositoryProtocol { public class CourseRepository: CourseRepositoryProtocol { private let api: API - private let appStorage: CoreStorage + private let appStorage: AppStorage private let config: Config private let persistence: CoursePersistenceProtocol public init(api: API, - appStorage: CoreStorage, + appStorage: AppStorage, config: Config, persistence: CoursePersistenceProtocol) { self.api = api @@ -39,7 +39,7 @@ public class CourseRepository: CourseRepositoryProtocol { } public func getCourseDetails(courseID: String) async throws -> CourseDetails { - let response = try await api.requestData(CourseEndpoint.getCourseDetail(courseID: courseID)) + let response = try await api.requestData(CourseDetailsEndpoint.getCourseDetail(courseID: courseID)) .mapResponse(DataLayer.CourseDetailsResponse.self) .domain(baseURL: config.baseURL.absoluteString) persistence.saveCourseDetails(course: response) @@ -52,7 +52,7 @@ public class CourseRepository: CourseRepositoryProtocol { public func getCourseBlocks(courseID: String) async throws -> CourseStructure { let course = try await api.requestData( - CourseEndpoint.getCourseBlocks(courseID: courseID, userName: appStorage.user?.username ?? "") + CourseDetailsEndpoint.getCourseBlocks(courseID: courseID, userName: appStorage.user?.username ?? "") ).mapResponse(DataLayer.CourseStructure.self) persistence.saveCourseStructure(structure: course) let parsedStructure = parseCourseStructure(course: course) @@ -65,7 +65,7 @@ public class CourseRepository: CourseRepositoryProtocol { } public func enrollToCourse(courseID: String) async throws -> Bool { - let enroll = try await api.request(CourseEndpoint.enrollToCourse(courseID: courseID)) + let enroll = try await api.request(CourseDetailsEndpoint.enrollToCourse(courseID: courseID)) if enroll.statusCode == 200 { return true } else { @@ -74,7 +74,7 @@ public class CourseRepository: CourseRepositoryProtocol { } public func blockCompletionRequest(courseID: String, blockID: String) async throws { - try await api.requestData(CourseEndpoint.blockCompletionRequest( + try await api.requestData(CourseDetailsEndpoint.blockCompletionRequest( username: appStorage.user?.username ?? "", courseID: courseID, blockID: blockID) @@ -82,18 +82,18 @@ public class CourseRepository: CourseRepositoryProtocol { } public func getHandouts(courseID: String) async throws -> String? { - return try await api.requestData(CourseEndpoint.getHandouts(courseID: courseID)) + return try await api.requestData(CourseDetailsEndpoint.getHandouts(courseID: courseID)) .mapResponse(DataLayer.HandoutsResponse.self) .handoutsHtml } public func getUpdates(courseID: String) async throws -> [CourseUpdate] { - return try await api.requestData(CourseEndpoint.getUpdates(courseID: courseID)) + return try await api.requestData(CourseDetailsEndpoint.getUpdates(courseID: courseID)) .mapResponse(DataLayer.CourseUpdates.self).map { $0.domain } } public func resumeBlock(courseID: String) async throws -> ResumeBlock { - return try await api.requestData(CourseEndpoint + return try await api.requestData(CourseDetailsEndpoint .resumeBlock(userName: appStorage.user?.username ?? "", courseID: courseID)) .mapResponse(DataLayer.ResumeBlock.self).domain } @@ -102,7 +102,7 @@ public class CourseRepository: CourseRepositoryProtocol { if let subtitlesOffline = persistence.loadSubtitles(url: url + selectedLanguage) { return subtitlesOffline } else { - let result = try await api.requestData(CourseEndpoint.getSubtitles( + let result = try await api.requestData(CourseDetailsEndpoint.getSubtitles( url: url, selectedLanguage: selectedLanguage )) diff --git a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift index 4e67cdfc3..8340e86ff 100644 --- a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift +++ b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift @@ -10,15 +10,13 @@ import CoreData import Core public extension DataLayer { - - typealias Blocks = [String: CourseBlock] - struct CourseStructure: Decodable { - public let rootItem: String - public var dict: Blocks - public let id: String - public let media: DataLayer.CourseMedia - public let certificate: Certificate? + let rootItem: String + typealias Blocks = [String: CourseBlock] + var dict: Blocks + let id: String + let media: DataLayer.CourseMedia + let certificate: Certificate? enum CodingKeys: String, CodingKey { case blocks @@ -28,7 +26,7 @@ public extension DataLayer { case certificate } - public init(rootItem: String, dict: Blocks, id: String, media: DataLayer.CourseMedia, certificate: Certificate?) { + init(rootItem: String, dict: Blocks, id: String, media: DataLayer.CourseMedia, certificate: Certificate?) { self.rootItem = rootItem self.dict = dict self.id = id @@ -49,40 +47,16 @@ public extension DataLayer { } public extension DataLayer { struct CourseBlock: Decodable { - public let blockId: String - public let id: String - public let graded: Bool - public let completion: Double? - public let studentUrl: String - public let type: String - public let displayName: String - public let descendants: [String]? - public let allSources: [String]? - public let userViewData: CourseDetailUserViewData? - - public init( - blockId: String, - id: String, - graded: Bool, - completion: Double?, - studentUrl: String, - type: String, - displayName: String, - descendants: [String]?, - allSources: [String]?, - userViewData: CourseDetailUserViewData? - ) { - self.blockId = blockId - self.id = id - self.graded = graded - self.completion = completion - self.studentUrl = studentUrl - self.type = type - self.displayName = displayName - self.descendants = descendants - self.allSources = allSources - self.userViewData = userViewData - } + let blockId: String + let id: String + let graded: Bool + let completion: Double? + let studentUrl: String + let type: String + let displayName: String + let descendants: [String]? + let allSources: [String]? + let userViewData: CourseDetailUserViewData? public enum CodingKeys: String, CodingKey { case id, type, descendants, graded, completion @@ -107,19 +81,9 @@ public extension DataLayer { } struct CourseDetailUserViewData: Decodable { - public let transcripts: [String: String]? - public let encodedVideo: CourseDetailEncodedVideoData? - public let topicID: String? - - public init( - transcripts: [String: String]?, - encodedVideo: CourseDetailEncodedVideoData?, - topicID: String? - ) { - self.transcripts = transcripts - self.encodedVideo = encodedVideo - self.topicID = topicID - } + let transcripts: [String: String]? + let encodedVideo: CourseDetailEncodedVideoData? + let topicID: String? public enum CodingKeys: String, CodingKey { case encodedVideo = "encoded_videos" @@ -129,16 +93,8 @@ public extension DataLayer { } struct CourseDetailEncodedVideoData: Decodable { - public let youTube: CourseDetailYouTubeData? - public let fallback: CourseDetailYouTubeData? - - public init( - youTube: CourseDetailYouTubeData?, - fallback: CourseDetailYouTubeData? - ) { - self.youTube = youTube - self.fallback = fallback - } + let youTube: CourseDetailYouTubeData? + let fallback: CourseDetailYouTubeData? enum CodingKeys: String, CodingKey { case youTube = "youtube" @@ -147,11 +103,7 @@ public extension DataLayer { } struct CourseDetailYouTubeData: Decodable { - public let url: String? - - public init(url: String?) { - self.url = url - } + let url: String? } } diff --git a/Course/Course/Data/Model/Data_UpdatesResponse.swift b/Course/Course/Data/Model/Data_UpdatesResponse.swift index bb9824e7b..88cdc883f 100644 --- a/Course/Course/Data/Model/Data_UpdatesResponse.swift +++ b/Course/Course/Data/Model/Data_UpdatesResponse.swift @@ -13,7 +13,7 @@ public extension DataLayer { public let id: Int public let date: String public let content: String - public let status: String? + public let status: String } typealias CourseUpdates = [CourseUpdate] } diff --git a/Course/Course/Data/Network/CourseEndpoint.swift b/Course/Course/Data/Network/CourseDetailsEndpoint.swift similarity index 98% rename from Course/Course/Data/Network/CourseEndpoint.swift rename to Course/Course/Data/Network/CourseDetailsEndpoint.swift index 7b3109a9c..57bb2459f 100644 --- a/Course/Course/Data/Network/CourseEndpoint.swift +++ b/Course/Course/Data/Network/CourseDetailsEndpoint.swift @@ -1,5 +1,5 @@ // -// CourseEndpoint.swift +// CourseDetailsEndpoint.swift // CourseDetails // // Created by  Stepanok Ivan on 26.09.2022. @@ -9,7 +9,7 @@ import Foundation import Core import Alamofire -enum CourseEndpoint: EndPointType { +enum CourseDetailsEndpoint: EndPointType { case getCourseDetail(courseID: String) case getCourseBlocks(courseID: String, userName: String) case pageHTML(pageUrlString: String) diff --git a/OpenEdX/Data/CoursePersistence.swift b/Course/Course/Data/Persistence/CoursePersistence.swift similarity index 76% rename from OpenEdX/Data/CoursePersistence.swift rename to Course/Course/Data/Persistence/CoursePersistence.swift index cd61e5e6e..1a9731e12 100644 --- a/OpenEdX/Data/CoursePersistence.swift +++ b/Course/Course/Data/Persistence/CoursePersistence.swift @@ -1,22 +1,38 @@ // // CoursePersistence.swift -// OpenEdX +// Course // -// Created by  Stepanok Ivan on 25.07.2023. +// Created by  Stepanok Ivan on 15.12.2022. // -import Foundation import CoreData -import Course import Core +public protocol CoursePersistenceProtocol { + func loadCourseDetails(courseID: String) throws -> CourseDetails + func saveCourseDetails(course: CourseDetails) + func loadEnrollments() throws -> [Core.CourseItem] + func saveEnrollments(items: [Core.CourseItem]) + func loadCourseStructure(courseID: String) throws -> DataLayer.CourseStructure + func saveCourseStructure(structure: DataLayer.CourseStructure) + func saveSubtitles(url: String, subtitlesString: String) + func loadSubtitles(url: String) -> String? + func clear() +} + public class CoursePersistence: CoursePersistenceProtocol { - private var context: NSManagedObjectContext + public init() {} - public init(context: NSManagedObjectContext) { - self.context = context - } + private let model = "CourseCoreModel" + + private lazy var persistentContainer: NSPersistentContainer = { + return createContainer() + }() + + private lazy var context: NSManagedObjectContext = { + return createContext() + }() public func loadCourseDetails(courseID: String) throws -> CourseDetails { let request = CDCourseDetails.fetchRequest() @@ -38,7 +54,7 @@ public class CoursePersistence: CoursePersistenceProtocol { public func saveCourseDetails(course: CourseDetails) { context.performAndWait { - let newCourseDetails = CDCourseDetails(context: self.context) + let newCourseDetails = CDCourseDetails(context: context) newCourseDetails.courseID = course.courseID newCourseDetails.org = course.org newCourseDetails.courseTitle = course.courseTitle @@ -157,7 +173,7 @@ public class CoursePersistence: CoursePersistenceProtocol { public func saveCourseStructure(structure: DataLayer.CourseStructure) { context.performAndWait { - let newStructure = CDCourseStructure(context: self.context) + let newStructure = CDCourseStructure(context: context) newStructure.certificate = structure.certificate?.url newStructure.mediaSmall = structure.media.image.small newStructure.mediaLarge = structure.media.image.large @@ -166,7 +182,7 @@ public class CoursePersistence: CoursePersistenceProtocol { newStructure.rootItem = structure.rootItem for block in Array(structure.dict.values) { - let courseDetail = CDCourseBlock(context: self.context) + let courseDetail = CDCourseBlock(context: context) courseDetail.allSources = block.allSources courseDetail.descendants = block.descendants courseDetail.graded = block.graded @@ -215,4 +231,48 @@ public class CoursePersistence: CoursePersistenceProtocol { } return nil } + + public func clear() { + let storeContainer = persistentContainer.persistentStoreCoordinator + for store in storeContainer.persistentStores { + do { + try storeContainer.destroyPersistentStore( + at: store.url!, + ofType: store.type, + options: nil + ) + } catch { + print("⛔️⛔️⛔️⛔️⛔️", error) + } + } + + // Re-create the persistent container + persistentContainer = createContainer() + context = createContext() + } + + private func createContainer() -> NSPersistentContainer { + let bundle = Bundle(for: Self.self) + let url = bundle.url(forResource: model, withExtension: "momd") + let managedObjectModel = NSManagedObjectModel(contentsOf: url!) + let container = NSPersistentContainer(name: model, managedObjectModel: managedObjectModel!) + container.loadPersistentStores(completionHandler: { (_, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + let description = NSPersistentStoreDescription() + description.shouldInferMappingModelAutomatically = true + description.shouldMigrateStoreAutomatically = true + container.persistentStoreDescriptions = [description] + + return container + } + + private func createContext() -> NSManagedObjectContext { + let context = persistentContainer.newBackgroundContext() + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + context.automaticallyMergesChangesFromParent = true + return context + } } diff --git a/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift b/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift deleted file mode 100644 index b17874645..000000000 --- a/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CoursePersistence.swift -// Course -// -// Created by  Stepanok Ivan on 15.12.2022. -// - -import CoreData -import Core - -public protocol CoursePersistenceProtocol { - func loadCourseDetails(courseID: String) throws -> CourseDetails - func saveCourseDetails(course: CourseDetails) - func loadEnrollments() throws -> [Core.CourseItem] - func saveEnrollments(items: [Core.CourseItem]) - func loadCourseStructure(courseID: String) throws -> DataLayer.CourseStructure - func saveCourseStructure(structure: DataLayer.CourseStructure) - func saveSubtitles(url: String, subtitlesString: String) - func loadSubtitles(url: String) -> String? -} - -public final class CourseBundle { - private init() {} -} - \ No newline at end of file diff --git a/Course/Course/Domain/Model/CourseDetails.swift b/Course/Course/Domain/Model/CourseDetails.swift index 0edb58854..b7adedbe8 100644 --- a/Course/Course/Domain/Model/CourseDetails.swift +++ b/Course/Course/Domain/Model/CourseDetails.swift @@ -8,18 +8,18 @@ import Foundation public struct CourseDetails { - public let courseID: String - public let org: String - public let courseTitle: String - public let courseDescription: String - public let courseStart: Date? - public let courseEnd: Date? - public let enrollmentStart: Date? - public let enrollmentEnd: Date? - public var isEnrolled: Bool - public var overviewHTML: String - public let courseBannerURL: String - public let courseVideoURL: String? + let courseID: String + let org: String + let courseTitle: String + let courseDescription: String + let courseStart: Date? + let courseEnd: Date? + let enrollmentStart: Date? + let enrollmentEnd: Date? + var isEnrolled: Bool + var overviewHTML: String + let courseBannerURL: String + let courseVideoURL: String? public init(courseID: String, org: String, diff --git a/Course/Course/Domain/Model/CourseUpdate.swift b/Course/Course/Domain/Model/CourseUpdate.swift index 2ef09f210..345d1e2a3 100644 --- a/Course/Course/Domain/Model/CourseUpdate.swift +++ b/Course/Course/Domain/Model/CourseUpdate.swift @@ -11,9 +11,9 @@ public struct CourseUpdate { public let id: Int public let date: String public var content: String - public let status: String? + public let status: String - public init(id: Int, date: String, content: String, status: String?) { + public init(id: Int, date: String, content: String, status: String) { self.id = id self.date = date self.content = content diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 574f71b81..69a9755f7 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -12,6 +12,12 @@ import Swinject public struct CourseContainerView: View { + @ObservedObject + private var viewModel: CourseContainerViewModel + @State private var selection: CourseTab = .course + private var courseID: String + private var title: String + enum CourseTab { case course case videos @@ -19,12 +25,6 @@ public struct CourseContainerView: View { case handounds } - @ObservedObject - private var viewModel: CourseContainerViewModel - @State private var selection: CourseTab = .course - private var courseID: String - private var title: String - public init( viewModel: CourseContainerViewModel, courseID: String, @@ -39,7 +39,7 @@ public struct CourseContainerView: View { } public var body: some View { - ZStack(alignment: .top) { + ZStack { if let courseStart = viewModel.courseStart { if courseStart > Date() { CourseOutlineView( @@ -61,6 +61,7 @@ public struct CourseContainerView: View { Text(CourseLocalization.CourseContainer.course) } .tag(CourseTab.course) + .hideNavigationBar() CourseOutlineView( viewModel: self.viewModel, @@ -73,6 +74,7 @@ public struct CourseContainerView: View { Text(CourseLocalization.CourseContainer.videos) } .tag(CourseTab.videos) + .hideNavigationBar() DiscussionTopicsView(courseID: courseID, viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self, @@ -83,6 +85,7 @@ public struct CourseContainerView: View { Text(CourseLocalization.CourseContainer.discussion) } .tag(CourseTab.discussion) + .hideNavigationBar() HandoutsView(courseID: courseID, viewModel: Container.shared.resolve(HandoutsViewModel.self, argument: courseID)!) @@ -91,6 +94,7 @@ public struct CourseContainerView: View { Text(CourseLocalization.CourseContainer.handouts) } .tag(CourseTab.handounds) + .hideNavigationBar() } .onFirstAppear { Task { @@ -99,11 +103,7 @@ public struct CourseContainerView: View { } } } - } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(titleBar()) - .onChange(of: selection, perform: { selection in + }.onChange(of: selection, perform: { selection in viewModel.trackSelectedTab( selection: selection, courseId: courseID, @@ -111,19 +111,6 @@ public struct CourseContainerView: View { ) }) } - - private func titleBar() -> String { - switch selection { - case .course: - return self.title - case .videos: - return self.title - case .discussion: - return DiscussionLocalization.title - case .handounds: - return CourseLocalization.CourseContainer.handouts - } - } } #if DEBUG diff --git a/Course/Course/Presentation/Details/CourseDetailsView.swift b/Course/Course/Presentation/Details/CourseDetailsView.swift index 168dd8de1..c6ba3b2e1 100644 --- a/Course/Course/Presentation/Details/CourseDetailsView.swift +++ b/Course/Course/Presentation/Details/CourseDetailsView.swift @@ -37,7 +37,17 @@ public struct CourseDetailsView: View { public var body: some View { ZStack(alignment: .top) { + + // MARK: - Page name VStack(alignment: .center) { + NavigationBar(title: CourseLocalization.Details.title, + leftButtonAction: { viewModel.router.back() }) + .onReceive(NotificationCenter + .Publisher(center: .default, + name: UIDevice.orientationDidChangeNotification)) { _ in + updateOrientation() + } + // MARK: - Page Body GeometryReader { proxy in if viewModel.isShowProgress { @@ -48,7 +58,7 @@ public struct CourseDetailsView: View { }.frame(width: proxy.size.width) } else { RefreshableScrollViewCompat(action: { - await viewModel.getCourseDetail(courseID: courseID, withProgress: false) + await viewModel.getCourseDetail(courseID: courseID, withProgress: isIOS14) }) { VStack(alignment: .leading) { if let courseDetails = viewModel.courseDetails { @@ -140,21 +150,12 @@ public struct CourseDetailsView: View { Spacer(minLength: 84) } } - }.padding(.top, 8) - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(CourseLocalization.Details.title) - - .onReceive(NotificationCenter - .Publisher(center: .default, - name: UIDevice.orientationDidChangeNotification)) { _ in - updateOrientation() } // MARK: - Offline mode SnackBar OfflineSnackBarView(connectivity: viewModel.connectivity, reloadAction: { - await viewModel.getCourseDetail(courseID: courseID, withProgress: false) + await viewModel.getCourseDetail(courseID: courseID, withProgress: isIOS14) }) // MARK: - Error Alert @@ -174,7 +175,7 @@ public struct CourseDetailsView: View { } } .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } @@ -255,7 +256,7 @@ private struct CourseTitleView: View { Text(courseDetails.org) .font(Theme.Fonts.labelMedium) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) .padding(.horizontal, 26) .padding(.top, 10) } diff --git a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift index 57b12b542..d16f4d8e5 100644 --- a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift @@ -18,6 +18,7 @@ public struct HandoutsUpdatesDetailView: View { private var handouts: String? private var announcements: [CourseUpdate]? private let title: String + @State private var height: [Int: CGFloat] = [:] public init( handouts: String?, @@ -67,65 +68,71 @@ public struct HandoutsUpdatesDetailView: View { public var body: some View { ZStack(alignment: .top) { - Theme.Colors.background - .ignoresSafeArea() GeometryReader { reader in - - // MARK: - Page Body - VStack(alignment: .leading) { + // MARK: - Page name + VStack(alignment: .center) { + NavigationBar( + title: title, + leftButtonAction: { router.back() } + ) - // MARK: - Handouts - if let handouts { - 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 { + // MARK: - Page Body + VStack(alignment: .leading) { - // MARK: - Announcements - ScrollView { - ForEach(Array(announcements.enumerated()), id: \.offset) { index, ann in - - Text(ann.date) - .font(Theme.Fonts.labelSmall) - let formattedAnnouncements = cssInjector.injectCSS( - colorScheme: colorScheme, - html: ann.content, - type: .discovery, - screenWidth: reader.size.width - ) - HStack { - HTMLFormattedText(formattedAnnouncements) - Spacer() - } - - .id(UUID()) - - if index != announcements.count - 1 { - Divider() + // MARK: - Handouts + if let handouts { + 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 { + + // MARK: - Announcements + ScrollView { + ForEach(Array(announcements.enumerated()), id: \.offset) { index, ann in + + 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] + ) + .frame(height: height[index]) + + if index != announcements.count - 1 { + Divider() + } } - } - }.frame(height: reader.size.height - 60) - } - }.padding(.top, 8) - .padding(.horizontal, 32) - .frame( - maxHeight: .infinity, - alignment: .topLeading) - .onRightSwipeGesture { - router.back() - } - Spacer(minLength: 84) + }.frame(height: reader.size.height - 60) + } + }.padding(.horizontal, 32) + .frame( + maxHeight: .infinity, + alignment: .topLeading) + .onRightSwipeGesture { + router.back() + } + Spacer(minLength: 84) + + }.background( + CoreAssets.background.swiftUIColor + .ignoresSafeArea() + ) } + } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(title) } } diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index d6563b46f..bbc0752e9 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -12,7 +12,7 @@ struct HandoutsView: View { private let courseID: String - @StateObject + @ObservedObject private var viewModel: HandoutsViewModel public init( @@ -20,13 +20,16 @@ struct HandoutsView: View { viewModel: HandoutsViewModel ) { self.courseID = courseID -// self.viewModel = viewModel - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel } public var body: some View { ZStack(alignment: .top) { + + // MARK: - Page name VStack(alignment: .center) { + NavigationBar(title: CourseLocalization.CourseContainer.handouts, + leftButtonAction: {viewModel.router.back() }) // MARK: - Page Body if viewModel.isShowProgress { @@ -38,9 +41,8 @@ struct HandoutsView: View { } else { VStack(alignment: .leading) { HandoutsItemCell(type: .handouts, onTapAction: { - guard let handouts = viewModel.handouts else { return } viewModel.router.showHandoutsUpdatesView( - handouts: handouts, + handouts: viewModel.handouts, announcements: nil, router: viewModel.router, cssInjector: viewModel.cssInjector) @@ -94,7 +96,7 @@ struct HandoutsView: View { } } .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } @@ -163,20 +165,20 @@ struct HandoutsItemCell: View { }, label: { HStack(spacing: 12) { type.image.renderingMode(.template) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .frame(width: 24, height: 24) VStack(alignment: .leading) { Text(type.title) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .font(Theme.Fonts.titleSmall) Text(type.description) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) .font(Theme.Fonts.labelSmall) } Spacer() Image(systemName: "chevron.right").resizable() .frame(width: 7, height: 12) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) } }).padding(.vertical, 16) diff --git a/Course/Course/Presentation/Outline/ContinueWithView.swift b/Course/Course/Presentation/Outline/ContinueWithView.swift index 610d9fee0..14bc3b874 100644 --- a/Course/Course/Presentation/Outline/ContinueWithView.swift +++ b/Course/Course/Presentation/Outline/ContinueWithView.swift @@ -35,7 +35,7 @@ struct ContinueWithView: View { HStack(alignment: .top) { VStack(alignment: .leading) { ContinueTitle(vertical: vertical) - }.foregroundColor(Theme.Colors.textPrimary) + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() UnitButtonView(type: .continueLesson, action: action) .frame(width: 200) @@ -44,7 +44,7 @@ struct ContinueWithView: View { } else { VStack(alignment: .leading) { ContinueTitle(vertical: vertical) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) } UnitButtonView(type: .continueLesson, action: action) } @@ -62,7 +62,7 @@ private struct ContinueTitle: View { var body: some View { Text(CoreLocalization.Courseware.continueWith) .font(Theme.Fonts.labelMedium) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) HStack { vertical.type.image Text(vertical.displayName) diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 19a6fa413..9a31759c9 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -11,7 +11,7 @@ import Kingfisher public struct CourseOutlineView: View { - @StateObject private var viewModel: CourseContainerViewModel + @ObservedObject private var viewModel: CourseContainerViewModel private let title: String private let courseID: String private let isVideo: Bool @@ -26,7 +26,7 @@ public struct CourseOutlineView: View { isVideo: Bool ) { self.title = title - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.courseID = courseID self.isVideo = isVideo } @@ -36,9 +36,14 @@ public struct CourseOutlineView: View { // MARK: - Page name GeometryReader { proxy in VStack(alignment: .center) { + NavigationBar( + title: title, + leftButtonAction: { viewModel.router.back() } + ) + // MARK: - Page Body RefreshableScrollViewCompat(action: { - await viewModel.getCourseBlocks(courseID: courseID, withProgress: false) + await viewModel.getCourseBlocks(courseID: courseID, withProgress: isIOS14) }) { VStack(alignment: .leading) { ZStack { @@ -55,7 +60,7 @@ public struct CourseOutlineView: View { // MARK: - Course Certificate if let certificate = viewModel.courseStructure?.certificate { if let url = certificate.url, url.count > 0 { - Theme.Colors.certificateForeground + CoreAssets.certificateForeground.swiftUIColor VStack(alignment: .center, spacing: 8) { CoreAssets.certificate.swiftUIImage Text(CourseLocalization.Outline.congratulations) @@ -71,7 +76,6 @@ public struct CourseOutlineView: View { ) .frame(width: 141) .padding(.top, 8) - .fullScreenCover( isPresented: $openCertificateView, content: { @@ -141,13 +145,13 @@ public struct CourseOutlineView: View { .onRightSwipeGesture { viewModel.router.back() } - }.padding(.top, 8) - + } + // MARK: - Offline mode SnackBar OfflineSnackBarView( connectivity: viewModel.connectivity, reloadAction: { - await viewModel.getCourseBlocks(courseID: courseID, withProgress: false) + await viewModel.getCourseBlocks(courseID: courseID, withProgress: isIOS14) } ) @@ -169,6 +173,7 @@ public struct CourseOutlineView: View { if viewModel.isShowProgress { VStack(alignment: .center) { ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) .padding(.horizontal) }.frame(maxWidth: .infinity, maxHeight: .infinity) @@ -176,7 +181,7 @@ public struct CourseOutlineView: View { } } .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } @@ -202,7 +207,7 @@ struct CourseStructureView: View { Text(chapter.displayName) .font(Theme.Fonts.titleMedium) .multilineTextAlignment(.leading) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) .padding(.horizontal, 24) .padding(.top, 40) ForEach(chapter.childs, id: \.id) { child in @@ -224,13 +229,7 @@ struct CourseStructureView: View { }, label: { Group { - if child.completion == 1 { - CoreAssets.finished.swiftUIImage - .renderingMode(.template) - .foregroundColor(.accentColor) - } else { - child.type.image - } + child.type.image Text(child.displayName) .font(Theme.Fonts.titleMedium) .multilineTextAlignment(.leading) @@ -241,7 +240,7 @@ struct CourseStructureView: View { : proxy.size.width * 0.6, alignment: .leading ) - }.foregroundColor(Theme.Colors.textPrimary) + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() if let state = viewModel.downloadState[child.id] { switch state { @@ -281,13 +280,13 @@ struct CourseStructureView: View { } } Image(systemName: "chevron.right") - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) }).padding(.horizontal, 36) .padding(.vertical, 20) if chapterIndex != chapters.count - 1 { Divider() .frame(height: 1) - .overlay(Theme.Colors.cardViewStroke) + .overlay(CoreAssets.cardViewStroke.swiftUIColor) .padding(.horizontal, 24) } } diff --git a/Course/Course/Presentation/Outline/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVerticalView.swift index c5cb290af..40ccd5153 100644 --- a/Course/Course/Presentation/Outline/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVerticalView.swift @@ -33,108 +33,112 @@ public struct CourseVerticalView: View { public var body: some View { ZStack(alignment: .top) { - // MARK: - Page Body - GeometryReader { proxy in - ScrollView { - VStack(alignment: .leading) { - // MARK: - Lessons list - ForEach(viewModel.verticals, id: \.id) { vertical in - if let index = viewModel.verticals.firstIndex(where: {$0.id == vertical.id}) { - Button(action: { - let vertical = viewModel.verticals[index] - if let block = vertical.childs.first { - viewModel.trackVerticalClicked( - courseId: courseID, - courseName: courseName, - vertical: vertical - ) - viewModel.router.showCourseUnit( - courseName: courseName, - blockId: block.id, - courseID: courseID, - 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(Theme.Colors.textPrimary) - 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 - ) - } + VStack(alignment: .center) { + NavigationBar(title: title, + leftButtonAction: { viewModel.router.back() }) + + // MARK: - Page Body + GeometryReader { proxy in + ScrollView { + VStack(alignment: .leading) { + // MARK: - Lessons list + ForEach(viewModel.verticals, id: \.id) { vertical in + if let index = viewModel.verticals.firstIndex(where: {$0.id == vertical.id}) { + Button(action: { + let vertical = viewModel.verticals[index] + if let block = vertical.childs.first { + viewModel.trackVerticalClicked( + courseId: courseID, + courseName: courseName, + vertical: vertical + ) + viewModel.router.showCourseUnit( + courseName: courseName, + blockId: block.id, + courseID: courseID, + 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(Theme.Colors.cardViewStroke) - .padding(.horizontal, 24) } } } - } - Spacer(minLength: 84) - }.frameLimit() - .onRightSwipeGesture { - viewModel.router.back() - } + Spacer(minLength: 84) + }.frameLimit() + .onRightSwipeGesture { + viewModel.router.back() + } + } } - .padding(.top, 8) // MARK: - Offline mode SnackBar OfflineSnackBarView(connectivity: viewModel.connectivity, @@ -156,11 +160,8 @@ public struct CourseVerticalView: View { } } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(title) .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index e9afacbc1..1479844a4 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -25,7 +25,6 @@ public struct CourseUnitView: View { } @State var offsetView: CGFloat = 0 @State var showDiscussion: Bool = false - @Environment(\.presentationMode) private var presentationMode private let sectionName: String public let playerStateSubject = CurrentValueSubject(nil) @@ -44,7 +43,13 @@ public struct CourseUnitView: View { ZStack(alignment: .bottom) { GeometryReader { reader in VStack(spacing: 0) { - VStack {}.frame(height: 100) + 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 @@ -53,58 +58,41 @@ public struct CourseUnitView: View { switch LessonType.from(block) { // MARK: YouTube case let .youtube(url, blockID): - if viewModel.connectivity.isInternetAvaliable { - YouTubeView( - name: block.displayName, - url: url, - courseID: viewModel.courseID, - blockID: blockID, - playerStateSubject: playerStateSubject, - languages: block.subtitles ?? [], - isOnScreen: index == viewModel.index - ).frameLimit() - Spacer(minLength: 100) - } else { - NoInternetView(playerStateSubject: playerStateSubject) - } + 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): - let url = viewModel.urlForVideoFileOrFallback( - blockId: blockID, - url: encodedUrl - ) - if viewModel.connectivity.isInternetAvaliable || url?.isFileURL == true { - EncodedVideoView( - name: block.displayName, - url: url, - courseID: viewModel.courseID, - blockID: blockID, - playerStateSubject: playerStateSubject, - languages: block.subtitles ?? [], - isOnScreen: index == viewModel.index - ).frameLimit() - Spacer(minLength: 100) - } else { - NoInternetView(playerStateSubject: playerStateSubject) - } + 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): - if viewModel.connectivity.isInternetAvaliable { - WebView(url: url, viewModel: viewModel) - } else { - NoInternetView(playerStateSubject: playerStateSubject) - } + WebView(url: url, viewModel: viewModel) // MARK: Unknown case .unknown(let url): - if viewModel.connectivity.isInternetAvaliable { UnknownView(url: url, viewModel: viewModel) Spacer() - } else { - NoInternetView(playerStateSubject: playerStateSubject) - } // MARK: Discussion case let .discussion(blockID, blockKey, title): - if viewModel.connectivity.isInternetAvaliable { VStack { if showDiscussion { DiscussionView( @@ -116,14 +104,16 @@ public struct CourseUnitView: View { ) Spacer(minLength: 100) } else { - VStack { - Color.clear - } + DiscussionView( + id: viewModel.courseID, + blockID: blockID, + blockKey: blockKey, + title: title, + viewModel: viewModel + ).drawingGroup() + Spacer(minLength: 100) } }.frameLimit() - } else { - NoInternetView(playerStateSubject: playerStateSubject) - } } } else { EmptyView() @@ -133,20 +123,34 @@ public struct CourseUnitView: View { .id(index) } } - .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 + .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 + } } } - } - - }) - + + }) + } 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) .clipped() @@ -165,7 +169,7 @@ public struct CourseUnitView: View { alertMessage = CourseLocalization.Alert.rotateDevice } Text(alertMessage ?? "") - }.shadowCardStyle(bgColor: Theme.Colors.accentColor, + }.shadowCardStyle(bgColor: CoreAssets.accentColor.swiftUIColor, textColor: .white) .transition(.move(edge: .bottom)) .onAppear { @@ -179,6 +183,13 @@ public struct CourseUnitView: View { // MARK: - Course Navigation VStack { + NavigationBar( + title: "", + leftButtonAction: { + viewModel.router.back() + playerStateSubject.send(VideoPlayerState.kill) + }).padding(.top, 50) + Spacer() CourseNavigationView( sectionName: sectionName, viewModel: viewModel, @@ -191,18 +202,9 @@ public struct CourseUnitView: View { viewModel.router.back() } } - .onDisappear { - if !presentationMode.wrappedValue.isPresented { - playerStateSubject.send(VideoPlayerState.kill) - } - } - } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle("") - .ignoresSafeArea() + }.ignoresSafeArea() .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } @@ -340,22 +342,3 @@ struct CourseUnitView_Previews: PreviewProvider { } //swiftlint:enable all #endif - -struct NoInternetView: View { - - let playerStateSubject: CurrentValueSubject - - var body: some 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) - } -} diff --git a/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift index 57a881589..37dcb67d3 100644 --- a/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift +++ b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift @@ -31,7 +31,7 @@ struct LessonProgressView: View { .foregroundColor( selected == viewModel.selectedLesson() ? .accentColor - : Theme.Colors.textSecondary + : CoreAssets.textSecondary.swiftUIColor ) } Spacer() diff --git a/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift b/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift index 94080fc32..8aeab7f13 100644 --- a/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift +++ b/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift @@ -37,7 +37,7 @@ struct YouTubeView: View { )! YouTubeVideoPlayer(viewModel: vm, isOnScreen: isOnScreen) Spacer(minLength: 100) - }.background(Theme.Colors.background) + }.background(CoreAssets.background.swiftUIColor) } } } diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index 251b59417..a3ddda18f 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -107,7 +107,7 @@ public struct EncodedVideoPlayer: View { HStack(spacing: 6) { CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template) Text(alertMessage) - }.shadowCardStyle(bgColor: Theme.Colors.snackbarInfoAlert, + }.shadowCardStyle(bgColor: CoreAssets.snackbarInfoAlert.swiftUIColor, textColor: .white) .transition(.move(edge: .bottom)) .onAppear { diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift index 01a27e640..ef856ff04 100644 --- a/Course/Course/Presentation/Video/PlayerViewController.swift +++ b/Course/Course/Presentation/Video/PlayerViewController.swift @@ -38,13 +38,6 @@ struct PlayerViewController: UIViewControllerRepresentable { self.seconds(seconds) } ) - - do { - try AVAudioSession.sharedInstance().setCategory(.playback) - } catch { - print(error.localizedDescription) - } - return controller } diff --git a/Course/Course/Presentation/Video/SubtittlesView.swift b/Course/Course/Presentation/Video/SubtittlesView.swift index 6501cb409..befc34f68 100644 --- a/Course/Course/Presentation/Video/SubtittlesView.swift +++ b/Course/Course/Presentation/Video/SubtittlesView.swift @@ -45,7 +45,7 @@ public struct SubtittlesView: View { Group { CoreAssets.sub.swiftUIImage.renderingMode(.template) Text(viewModel.generateLanguageName(code: viewModel.selectedLanguage ?? "")) - }.foregroundColor(Theme.Colors.accentColor) + }.foregroundColor(CoreAssets.accentColor.swiftUIColor) .font(Theme.Fonts.labelLarge) }) } @@ -60,8 +60,8 @@ public struct SubtittlesView: View { .padding(.vertical, 16) .font(Theme.Fonts.bodyMedium) .foregroundColor(subtitle.fromTo.contains(Date(milliseconds: currentTime)) - ? Theme.Colors.textPrimary - : Theme.Colors.textSecondary) + ? CoreAssets.textPrimary.swiftUIColor + : CoreAssets.textSecondary.swiftUIColor) .onChange(of: currentTime, perform: { _ in if subtitle.fromTo.contains(Date(milliseconds: currentTime)) { if id != subtitle.id { diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index cddcdba6c..7aab1c567 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -50,7 +50,7 @@ public class VideoPlayerViewModel: ObservableObject { @MainActor func blockCompletionRequest() async { - let fullBlockID = "block-v1:\(courseID.dropFirst(10))+type@video+block@\(blockID)" + let fullBlockID = "block-v1:\(courseID.dropFirst(10))+type@discussion+block@\(blockID)" do { try await interactor.blockCompletionRequest(courseID: courseID, blockID: fullBlockID) } catch let error { diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index f3a886c72..b8cc5d335 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -84,7 +84,7 @@ public struct YouTubeVideoPlayer: View { HStack(spacing: 6) { CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template) Text(alertMessage) - }.shadowCardStyle(bgColor: Theme.Colors.snackbarInfoAlert, + }.shadowCardStyle(bgColor: CoreAssets.snackbarInfoAlert.swiftUIColor, textColor: .white) .transition(.move(edge: .bottom)) .onAppear { diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index a4cf6b418..bdf04ef71 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -2178,12 +2178,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { perform?(`blocks`) } - open func deleteAllFiles() { - addInvocation(.m_deleteAllFiles) - let perform = methodPerformValue(.m_deleteAllFiles) as? () -> Void - perform?() - } - open func fileUrl(for blockId: String) -> URL? { addInvocation(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) let perform = methodPerformValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void @@ -2206,7 +2200,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_resumeDownloading case m_pauseDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) - case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2238,8 +2231,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_deleteAllFiles, .m_deleteAllFiles): return .match - case (.m_fileUrl__for_blockId(let lhsBlockid), .m_fileUrl__for_blockId(let rhsBlockid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) @@ -2257,7 +2248,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return 0 case .m_pauseDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue - case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue } } @@ -2270,7 +2260,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return ".resumeDownloading()" case .m_pauseDownloading: return ".pauseDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" - case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" } } @@ -2357,7 +2346,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} - public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} } @@ -2386,9 +2374,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } - public static func deleteAllFiles(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_deleteAllFiles, performs: perform) - } public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } diff --git a/Dashboard/Dashboard.xcodeproj/project.pbxproj b/Dashboard/Dashboard.xcodeproj/project.pbxproj index 9eb3ade8c..4aee39a87 100644 --- a/Dashboard/Dashboard.xcodeproj/project.pbxproj +++ b/Dashboard/Dashboard.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 027DB34328D8E89B002B6862 /* DashboardInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB34228D8E89B002B6862 /* DashboardInteractor.swift */; }; 027DB34528D8E9D2002B6862 /* DashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */; }; 02A48B18295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02A48B16295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld */; }; - 02A48B1A295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A48B19295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift */; }; + 02A48B1A295ACE3D0033D5E0 /* DashboardPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A48B19295ACE3D0033D5E0 /* DashboardPersistence.swift */; }; 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 */; }; @@ -45,7 +45,7 @@ 027DB34228D8E89B002B6862 /* DashboardInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardInteractor.swift; sourceTree = ""; }; 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewModel.swift; sourceTree = ""; }; 02A48B17295ACE200033D5E0 /* DashboardCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DashboardCoreModel.xcdatamodel; sourceTree = ""; }; - 02A48B19295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPersistenceProtocol.swift; sourceTree = ""; }; + 02A48B19295ACE3D0033D5E0 /* DashboardPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPersistence.swift; sourceTree = ""; }; 02A9A9082978194100B55797 /* DashboardTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DashboardTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02A9A90A2978194100B55797 /* DashboardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewModelTests.swift; sourceTree = ""; }; 02A9A92829781A4D00B55797 /* DashboardMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardMock.generated.swift; sourceTree = ""; }; @@ -101,7 +101,7 @@ 0208666929CC6D0F00BC05B2 /* Persistence */ = { isa = PBXGroup; children = ( - 02A48B19295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift */, + 02A48B19295ACE3D0033D5E0 /* DashboardPersistence.swift */, 02A48B16295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld */, ); path = Persistence; @@ -443,7 +443,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 02A48B1A295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift in Sources */, + 02A48B1A295ACE3D0033D5E0 /* DashboardPersistence.swift in Sources */, 027DB33D28D8DB5E002B6862 /* DashboardRepository.swift in Sources */, 02A48B18295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld in Sources */, 02F175332A4DABBF0019CD70 /* DashboardAnalytics.swift in Sources */, @@ -488,7 +488,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -509,7 +509,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -530,7 +530,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -551,7 +551,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -572,7 +572,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -593,7 +593,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -685,7 +685,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -713,7 +713,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -799,7 +799,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -826,7 +826,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -976,7 +976,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1011,7 +1011,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1109,7 +1109,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1202,7 +1202,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1300,7 +1300,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1393,7 +1393,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Dashboard/Dashboard/Data/DashboardRepository.swift b/Dashboard/Dashboard/Data/DashboardRepository.swift index ce5721784..0537afea9 100644 --- a/Dashboard/Dashboard/Data/DashboardRepository.swift +++ b/Dashboard/Dashboard/Data/DashboardRepository.swift @@ -16,20 +16,20 @@ public protocol DashboardRepositoryProtocol { public class DashboardRepository: DashboardRepositoryProtocol { private let api: API - private let storage: CoreStorage + private let appStorage: AppStorage private let config: Config private let persistence: DashboardPersistenceProtocol - public init(api: API, storage: CoreStorage, config: Config, persistence: DashboardPersistenceProtocol) { + public init(api: API, appStorage: AppStorage, config: Config, persistence: DashboardPersistenceProtocol) { self.api = api - self.storage = storage + self.appStorage = appStorage self.config = config self.persistence = persistence } public func getMyCourses(page: Int) async throws -> [CourseItem] { let result = try await api.requestData( - DashboardEndpoint.getMyCourses(username: storage.user?.username ?? "", page: page) + DashboardEndpoint.getMyCourses(username: appStorage.user?.username ?? "", page: page) ) .mapResponse(DataLayer.CourseEnrollments.self) .domain(baseURL: config.baseURL.absoluteString) diff --git a/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift b/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift index d9e4dec06..02903fab6 100644 --- a/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift +++ b/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift @@ -11,25 +11,25 @@ import Alamofire enum DashboardEndpoint: EndPointType { case getMyCourses(username: String, page: Int) - + var path: String { switch self { case let .getMyCourses(username, _): return "/mobile_api_extensions/v1/users/\(username)/course_enrollments" } } - + var httpMethod: HTTPMethod { switch self { case .getMyCourses: return .get } } - + var headers: HTTPHeaders? { nil } - + var task: HTTPTask { switch self { case let .getMyCourses(_, page): diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents b/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents index eeee515fe..60c42556d 100644 --- a/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents +++ b/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents @@ -1,6 +1,6 @@ - - + + diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardPersistence.swift b/Dashboard/Dashboard/Data/Persistence/DashboardPersistence.swift new file mode 100644 index 000000000..f2b109d76 --- /dev/null +++ b/Dashboard/Dashboard/Data/Persistence/DashboardPersistence.swift @@ -0,0 +1,120 @@ +// +// DashboardPersistence.swift +// Dashboard +// +// Created by  Stepanok Ivan on 27.12.2022. +// + +import CoreData +import Core + +public protocol DashboardPersistenceProtocol { + func loadMyCourses() throws -> [CourseItem] + func saveMyCourses(items: [CourseItem]) + func clear() +} + +public class DashboardPersistence: DashboardPersistenceProtocol { + + private let model = "DashboardCoreModel" + + private lazy var persistentContainer: NSPersistentContainer = { + return createContainer() + }() + + private lazy var context: NSManagedObjectContext = { + return createContext() + }() + + public init() {} + + public func loadMyCourses() throws -> [CourseItem] { + let result = try? context.fetch(CDCourseItem.fetchRequest()) + .map { CourseItem(name: $0.name ?? "", + org: $0.org ?? "", + shortDescription: $0.desc ?? "", + imageURL: $0.imageURL ?? "", + isActive: nil, + courseStart: $0.courseStart, + courseEnd: $0.courseEnd, + enrollmentStart: $0.enrollmentStart, + enrollmentEnd: $0.enrollmentEnd, + courseID: $0.courseID ?? "", + numPages: Int($0.numPages), + coursesCount: Int($0.courseCount))} + if let result, !result.isEmpty { + return result + } else { + throw NoCachedDataError() + } + } + + public func saveMyCourses(items: [CourseItem]) { + for item in items { + context.performAndWait { + let newItem = CDCourseItem(context: context) + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + newItem.name = item.name + newItem.org = item.org + newItem.desc = item.shortDescription + newItem.imageURL = item.imageURL + newItem.courseStart = item.courseStart + newItem.courseEnd = item.courseEnd + newItem.enrollmentStart = item.enrollmentStart + newItem.enrollmentEnd = item.enrollmentEnd + newItem.numPages = Int32(item.numPages) + newItem.courseID = item.courseID + + do { + try context.save() + } catch { + print("⛔️⛔️⛔️⛔️⛔️", error) + } + } + } + } + + public func clear() { + let storeContainer = persistentContainer.persistentStoreCoordinator + for store in storeContainer.persistentStores { + do { + try storeContainer.destroyPersistentStore( + at: store.url!, + ofType: store.type, + options: nil + ) + } catch { + print("⛔️⛔️⛔️⛔️⛔️", error) + } + } + + // Re-create the persistent container + persistentContainer = createContainer() + context = createContext() + } + + private func createContainer() -> NSPersistentContainer { + let bundle = Bundle(for: Self.self) + let url = bundle.url(forResource: model, withExtension: "momd") + let managedObjectModel = NSManagedObjectModel(contentsOf: url!) + let container = NSPersistentContainer(name: model, managedObjectModel: managedObjectModel!) + container.loadPersistentStores(completionHandler: { (_, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + let description = NSPersistentStoreDescription() + description.shouldInferMappingModelAutomatically = true + description.shouldMigrateStoreAutomatically = true + container.persistentStoreDescriptions = [description] + + return container + } + + private func createContext() -> NSManagedObjectContext { + let context = persistentContainer.newBackgroundContext() + context.automaticallyMergesChangesFromParent = true + return context + } + +} diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift deleted file mode 100644 index 14bad2aaa..000000000 --- a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// DashboardPersistence.swift -// Dashboard -// -// Created by  Stepanok Ivan on 27.12.2022. -// - -import CoreData -import Core - -public protocol DashboardPersistenceProtocol { - func loadMyCourses() throws -> [CourseItem] - func saveMyCourses(items: [CourseItem]) -} - -public final class DashboardBundle { - private init() {} -} diff --git a/Dashboard/Dashboard/Presentation/DashboardView.swift b/Dashboard/Dashboard/Presentation/DashboardView.swift index 4be6e62e7..274a77ecb 100644 --- a/Dashboard/Dashboard/Presentation/DashboardView.swift +++ b/Dashboard/Dashboard/Presentation/DashboardView.swift @@ -12,31 +12,39 @@ public struct DashboardView: View { private let dashboardCourses: some View = VStack(alignment: .leading) { Text(DashboardLocalization.Header.courses) .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Text(DashboardLocalization.Header.welcomeBack) .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) }.listRowBackground(Color.clear) .padding(.top, 24) - @StateObject + @ObservedObject private var viewModel: DashboardViewModel private let router: DashboardRouter public init(viewModel: DashboardViewModel, router: DashboardRouter) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.router = router + Task { + await viewModel.getMyCourses(page: 1) + } } public var body: some View { ZStack(alignment: .top) { - // MARK: - Page body + // MARK: - Page name VStack(alignment: .center) { - RefreshableScrollViewCompat(action: { - await viewModel.getMyCourses(page: 1, refresh: true) - }) { - Group { + ZStack { + Text(DashboardLocalization.title) + .titleSettings() + } + + ZStack { + RefreshableScrollViewCompat(action: { + await viewModel.getMyCourses(page: 1, refresh: true) + }) { if viewModel.courses.isEmpty && !viewModel.fetchInProgress { EmptyPageIcon() } else { @@ -90,9 +98,9 @@ public struct DashboardView: View { VStack {}.frame(height: 40) } } - } - }.frameLimit() - }.padding(.top, 8) + }.frameLimit() + } + } // MARK: - Offline mode SnackBar OfflineSnackBarView(connectivity: viewModel.connectivity, @@ -116,13 +124,8 @@ public struct DashboardView: View { } } } - .onFirstAppear { - Task { - await viewModel.getMyCourses(page: 1) - } - } .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } @@ -156,11 +159,11 @@ struct EmptyPageIcon: View { .padding(.bottom, 16) Text(DashboardLocalization.Empty.title) .font(Theme.Fonts.titleMedium) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.bottom, 8) Text(DashboardLocalization.Empty.subtitle) .font(Theme.Fonts.bodySmall) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) } .padding(.top, 200) } diff --git a/Dashboard/Dashboard/Presentation/DashboardViewModel.swift b/Dashboard/Dashboard/Presentation/DashboardViewModel.swift index 6e4d9974a..16ddff151 100644 --- a/Dashboard/Dashboard/Presentation/DashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/DashboardViewModel.swift @@ -43,7 +43,7 @@ public class DashboardViewModel: ObservableObject { .sink { [weak self] _ in guard let self = self else { return } Task { - await self.getMyCourses(page: 1, refresh: true) + await self.getMyCourses(page: 1) } } } diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index 27aebe250..b703eace7 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -1536,12 +1536,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { perform?(`blocks`) } - open func deleteAllFiles() { - addInvocation(.m_deleteAllFiles) - let perform = methodPerformValue(.m_deleteAllFiles) as? () -> Void - perform?() - } - open func fileUrl(for blockId: String) -> URL? { addInvocation(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) let perform = methodPerformValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void @@ -1564,7 +1558,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_resumeDownloading case m_pauseDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) - case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -1596,8 +1589,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_deleteAllFiles, .m_deleteAllFiles): return .match - case (.m_fileUrl__for_blockId(let lhsBlockid), .m_fileUrl__for_blockId(let rhsBlockid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) @@ -1615,7 +1606,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return 0 case .m_pauseDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue - case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue } } @@ -1628,7 +1618,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return ".resumeDownloading()" case .m_pauseDownloading: return ".pauseDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" - case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" } } @@ -1715,7 +1704,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} - public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} } @@ -1744,9 +1732,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } - public static func deleteAllFiles(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_deleteAllFiles, performs: perform) - } public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } diff --git a/Discovery/Discovery.xcodeproj/project.pbxproj b/Discovery/Discovery.xcodeproj/project.pbxproj index 05974ad4f..59b0d96ae 100644 --- a/Discovery/Discovery.xcodeproj/project.pbxproj +++ b/Discovery/Discovery.xcodeproj/project.pbxproj @@ -15,7 +15,7 @@ 0284DBFC28D4856A00830893 /* DiscoveryEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0284DBFB28D4856A00830893 /* DiscoveryEndpoint.swift */; }; 0284DC0328D4922900830893 /* DiscoveryRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0284DC0228D4922900830893 /* DiscoveryRepository.swift */; }; 029737402949FB070051696B /* DiscoveryCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0297373E2949FB070051696B /* DiscoveryCoreModel.xcdatamodeld */; }; - 029737422949FB3B0051696B /* DiscoveryPersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029737412949FB3B0051696B /* DiscoveryPersistenceProtocol.swift */; }; + 029737422949FB3B0051696B /* DiscoveryPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029737412949FB3B0051696B /* DiscoveryPersistence.swift */; }; 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 */; }; @@ -50,7 +50,7 @@ 0284DBFB28D4856A00830893 /* DiscoveryEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryEndpoint.swift; sourceTree = ""; }; 0284DC0228D4922900830893 /* DiscoveryRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryRepository.swift; sourceTree = ""; }; 0297373F2949FB070051696B /* DiscoveryCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DiscoveryCoreModel.xcdatamodel; sourceTree = ""; }; - 029737412949FB3B0051696B /* DiscoveryPersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPersistenceProtocol.swift; sourceTree = ""; }; + 029737412949FB3B0051696B /* DiscoveryPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPersistence.swift; sourceTree = ""; }; 02ED50C729A649C9008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02ED50C829A649C9008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 02EF39D028D867690058F6BD /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; @@ -110,7 +110,7 @@ 0208666829CC6CD600BC05B2 /* Persistence */ = { isa = PBXGroup; children = ( - 029737412949FB3B0051696B /* DiscoveryPersistenceProtocol.swift */, + 029737412949FB3B0051696B /* DiscoveryPersistence.swift */, 0297373E2949FB070051696B /* DiscoveryCoreModel.xcdatamodeld */, ); path = Persistence; @@ -466,7 +466,7 @@ 02F3BFDF29252F2F0051930C /* DiscoveryRouter.swift in Sources */, 0283347928D49A8700C828FC /* DiscoveryViewModel.swift in Sources */, 072787B428D34D91002E9142 /* DiscoveryView.swift in Sources */, - 029737422949FB3B0051696B /* DiscoveryPersistenceProtocol.swift in Sources */, + 029737422949FB3B0051696B /* DiscoveryPersistence.swift in Sources */, 0284DC0328D4922900830893 /* DiscoveryRepository.swift in Sources */, 02EF39DC28D86BEF0058F6BD /* Strings.swift in Sources */, 02F1752F2A4DA3B60019CD70 /* DiscoveryAnalytics.swift in Sources */, @@ -514,7 +514,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -535,7 +535,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -556,7 +556,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -577,7 +577,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -598,7 +598,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -619,7 +619,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -711,7 +711,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -739,7 +739,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -825,7 +825,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -852,7 +852,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -1002,7 +1002,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1037,7 +1037,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1135,7 +1135,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1234,7 +1234,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1327,7 +1327,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1419,7 +1419,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Discovery/Discovery/Data/DiscoveryRepository.swift b/Discovery/Discovery/Data/DiscoveryRepository.swift index 4ba3fa74b..021293fbf 100644 --- a/Discovery/Discovery/Data/DiscoveryRepository.swift +++ b/Discovery/Discovery/Data/DiscoveryRepository.swift @@ -19,12 +19,12 @@ public protocol DiscoveryRepositoryProtocol { public class DiscoveryRepository: DiscoveryRepositoryProtocol { private let api: API - private let appStorage: CoreStorage + private let appStorage: AppStorage private let config: Config private let persistence: DiscoveryPersistenceProtocol public init(api: API, - appStorage: CoreStorage, + appStorage: AppStorage, config: Config, persistence: DiscoveryPersistenceProtocol) { self.api = api diff --git a/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents b/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents index dc2f9ce96..548b2b57d 100644 --- a/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents +++ b/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents @@ -1,6 +1,6 @@ - - + + diff --git a/Discovery/Discovery/Data/Persistence/DiscoveryPersistence.swift b/Discovery/Discovery/Data/Persistence/DiscoveryPersistence.swift new file mode 100644 index 000000000..55b0346f0 --- /dev/null +++ b/Discovery/Discovery/Data/Persistence/DiscoveryPersistence.swift @@ -0,0 +1,122 @@ +// +// DiscoveryPersistence.swift +// Discovery +// +// Created by  Stepanok Ivan on 14.12.2022. +// + +import CoreData +import Core + +public protocol DiscoveryPersistenceProtocol { + func loadDiscovery() throws -> [CourseItem] + func saveDiscovery(items: [CourseItem]) + func clear() +} + +public class DiscoveryPersistence: DiscoveryPersistenceProtocol { + + private let model = "DiscoveryCoreModel" + + private lazy var persistentContainer: NSPersistentContainer = { + return createContainer() + }() + + private lazy var context: NSManagedObjectContext = { + return createContext() + }() + + public init() {} + + public func loadDiscovery() throws -> [CourseItem] { + let result = try? context.fetch(CDCourseItem.fetchRequest()) + .map { CourseItem(name: $0.name ?? "", + org: $0.org ?? "", + shortDescription: $0.desc ?? "", + imageURL: $0.imageURL ?? "", + isActive: $0.isActive, + courseStart: $0.courseStart, + courseEnd: $0.courseEnd, + enrollmentStart: $0.enrollmentStart, + enrollmentEnd: $0.enrollmentEnd, + courseID: $0.courseID ?? "", + numPages: Int($0.numPages), + coursesCount: Int($0.courseCount))} + if let result, !result.isEmpty { + return result + } else { + throw NoCachedDataError() + } + } + + public func saveDiscovery(items: [CourseItem]) { + for item in items { + context.performAndWait { + let newItem = CDCourseItem(context: context) + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + newItem.name = item.name + newItem.org = item.org + newItem.desc = item.shortDescription + newItem.imageURL = item.imageURL + if let isActive = item.isActive { + newItem.isActive = isActive + } + newItem.courseStart = item.courseStart + newItem.courseEnd = item.courseEnd + newItem.enrollmentStart = item.enrollmentStart + newItem.enrollmentEnd = item.enrollmentEnd + newItem.numPages = Int32(item.numPages) + newItem.courseID = item.courseID + + do { + try context.save() + } catch { + print("⛔️⛔️⛔️⛔️⛔️", error) + } + } + } + } + + public func clear() { + let storeContainer = persistentContainer.persistentStoreCoordinator + for store in storeContainer.persistentStores { + do { + try storeContainer.destroyPersistentStore( + at: store.url!, + ofType: store.type, + options: nil + ) + } catch { + print("⛔️⛔️⛔️⛔️⛔️", error) + } + } + + // Re-create the persistent container + persistentContainer = createContainer() + context = createContext() + } + + private func createContainer() -> NSPersistentContainer { + let bundle = Bundle(for: Self.self) + let url = bundle.url(forResource: model, withExtension: "momd") + let managedObjectModel = NSManagedObjectModel(contentsOf: url!) + let container = NSPersistentContainer(name: model, managedObjectModel: managedObjectModel!) + container.loadPersistentStores(completionHandler: { (_, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + let description = NSPersistentStoreDescription() + description.shouldInferMappingModelAutomatically = true + description.shouldMigrateStoreAutomatically = true + container.persistentStoreDescriptions = [description] + + return container + } + + private func createContext() -> NSManagedObjectContext { + let context = persistentContainer.newBackgroundContext() + context.automaticallyMergesChangesFromParent = true + return context + } +} diff --git a/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift b/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift deleted file mode 100644 index a18338e3b..000000000 --- a/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// DiscoveryPersistence.swift -// Discovery -// -// Created by  Stepanok Ivan on 14.12.2022. -// - -import CoreData -import Core - -public protocol DiscoveryPersistenceProtocol { - func loadDiscovery() throws -> [CourseItem] - func saveDiscovery(items: [CourseItem]) -} - -public final class DiscoveryBundle { - private init() {} -} diff --git a/Discovery/Discovery/Presentation/DiscoveryView.swift b/Discovery/Discovery/Presentation/DiscoveryView.swift index 8ca698572..043170fc7 100644 --- a/Discovery/Discovery/Presentation/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/DiscoveryView.swift @@ -10,7 +10,7 @@ import Core public struct DiscoveryView: View { - @StateObject + @ObservedObject private var viewModel: DiscoveryViewModel private let router: DiscoveryRouter @State private var isRefreshing: Bool = false @@ -18,15 +18,18 @@ public struct DiscoveryView: View { private let discoveryNew: some View = VStack(alignment: .leading) { Text(DiscoveryLocalization.Header.title1) .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Text(DiscoveryLocalization.Header.title2) .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) }.listRowBackground(Color.clear) public init(viewModel: DiscoveryViewModel, router: DiscoveryRouter) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.router = router + Task { + await viewModel.discovery(page: 1) + } } public var body: some View { @@ -34,6 +37,10 @@ public struct DiscoveryView: View { // MARK: - Page name VStack(alignment: .center) { + ZStack { + Text(DiscoveryLocalization.title) + .titleSettings(top: 10) + } // MARK: - Search fake field HStack(spacing: 11) { @@ -41,7 +48,7 @@ public struct DiscoveryView: View { .padding(.leading, 16) .padding(.top, 1) Text(DiscoveryLocalization.search) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) Spacer() } .onTapGesture { @@ -52,12 +59,12 @@ public struct DiscoveryView: View { .frame(maxWidth: 532) .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputUnfocusedBackground) + .fill(CoreAssets.textInputUnfocusedBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputUnfocusedStroke) + .fill(CoreAssets.textInputUnfocusedStroke.swiftUIColor) ).onTapGesture { router.showDiscoverySearch() viewModel.discoverySearchBarClicked() @@ -67,12 +74,11 @@ public struct DiscoveryView: View { ZStack { RefreshableScrollViewCompat(action: { + viewModel.courses = [] viewModel.totalPages = 1 viewModel.nextPage = 1 - Task { - await viewModel.discovery(page: 1, withProgress: false) - } - }) { + await viewModel.discovery(page: 1) + }) { LazyVStack(spacing: 0) { HStack { discoveryNew @@ -80,28 +86,25 @@ public struct DiscoveryView: View { .padding(.bottom, 20) Spacer() }.padding(.leading, 10) - ForEach(Array(viewModel.courses.enumerated()), id: \.offset) { index, course in - CourseCellView( - model: course, - type: .discovery, - index: index, - cellsCount: viewModel.courses.count - ).padding(.horizontal, 24) - .onAppear { - Task { - await viewModel.getDiscoveryCourses(index: index) - } - } - .onTapGesture { - viewModel.discoveryCourseClicked( - courseID: course.courseID, - courseName: course.name - ) - router.showCourseDetais( - courseID: course.courseID, - title: course.name - ) + ForEach(Array(viewModel.courses.enumerated()), + id: \.offset) { index, course in + CourseCellView(model: course, + type: .discovery, + index: index, + cellsCount: viewModel.courses.count) + .padding(.horizontal, 24) + .onAppear { + Task { + await viewModel.getDiscoveryCourses(index: index) } + } + .onTapGesture { + viewModel.discoveryCourseClicked(courseID: course.courseID, courseName: course.name) + router.showCourseDetais( + courseID: course.courseID, + title: course.name + ) + } } // MARK: - ProgressBar @@ -116,14 +119,16 @@ public struct DiscoveryView: View { } }.frameLimit() } - }.padding(.top, 8) + } // MARK: - Offline mode SnackBar - OfflineSnackBarView( - connectivity: viewModel.connectivity, - reloadAction: { - await viewModel.discovery(page: 1, withProgress: false) - }) + OfflineSnackBarView(connectivity: viewModel.connectivity, + reloadAction: { + viewModel.courses = [] + viewModel.totalPages = 1 + viewModel.nextPage = 1 + await viewModel.discovery(page: 1, withProgress: isIOS14) + }) // MARK: - Error Alert if viewModel.showError { @@ -141,12 +146,7 @@ public struct DiscoveryView: View { } } } - .onFirstAppear { - Task { - await viewModel.discovery(page: 1) - } - } - .background(Theme.Colors.background.ignoresSafeArea()) + .background(CoreAssets.background.swiftUIColor.ignoresSafeArea()) } } diff --git a/Discovery/Discovery/Presentation/DiscoveryViewModel.swift b/Discovery/Discovery/Presentation/DiscoveryViewModel.swift index 37514275b..c99e2e3f8 100644 --- a/Discovery/Discovery/Presentation/DiscoveryViewModel.swift +++ b/Discovery/Discovery/Presentation/DiscoveryViewModel.swift @@ -30,11 +30,9 @@ public class DiscoveryViewModel: ObservableObject { private let interactor: DiscoveryInteractorProtocol private let analytics: DiscoveryAnalytics - public init( - interactor: DiscoveryInteractorProtocol, - connectivity: ConnectivityProtocol, - analytics: DiscoveryAnalytics - ) { + public init(interactor: DiscoveryInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DiscoveryAnalytics) { self.interactor = interactor self.connectivity = connectivity self.analytics = analytics @@ -60,13 +58,7 @@ public class DiscoveryViewModel: ObservableObject { fetchInProgress = withProgress do { if connectivity.isInternetAvaliable { - if page == 1 { - await courses = try interactor.discovery(page: page) - self.totalPages = 1 - self.nextPage = 1 - } else { - await courses += try interactor.discovery(page: page) - } + await courses += try interactor.discovery(page: page) self.nextPage += 1 if !courses.isEmpty { totalPages = courses[0].numPages diff --git a/Discovery/Discovery/Presentation/SearchView.swift b/Discovery/Discovery/Presentation/SearchView.swift index 09e4619cf..b330d8c5e 100644 --- a/Discovery/Discovery/Presentation/SearchView.swift +++ b/Discovery/Discovery/Presentation/SearchView.swift @@ -27,7 +27,7 @@ public struct SearchView: View { NavigationBar(title: DiscoveryLocalization.search, leftButtonAction: { viewModel.router.backWithFade() - }).padding(.bottom, -7) + }) HStack(spacing: 11) { Image(systemName: "magnifyingglass") @@ -35,8 +35,8 @@ public struct SearchView: View { .padding(.top, -1) .foregroundColor( viewModel.isSearchActive - ? Theme.Colors.accentColor - : Theme.Colors.textPrimary + ? CoreAssets.accentColor.swiftUIColor + : CoreAssets.textPrimary.swiftUIColor ) TextField( @@ -54,7 +54,7 @@ public struct SearchView: View { self.becomeFirstResponderRunOnce = true } }) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() if !viewModel.searchText.trimmingCharacters(in: .whitespaces).isEmpty { Button(action: { viewModel.searchText.removeAll() }, label: { @@ -64,23 +64,24 @@ public struct SearchView: View { .frame(height: 24) .padding(.horizontal) }) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) } } + .padding(.top, 3) .frame(minHeight: 48) .frame(maxWidth: 532) .background( Theme.Shapes.textInputShape .fill(viewModel.isSearchActive - ? Theme.Colors.textInputBackground - : Theme.Colors.textInputUnfocusedBackground) + ? CoreAssets.textInputBackground.swiftUIColor + : CoreAssets.textInputUnfocusedBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) .fill(viewModel.isSearchActive - ? Theme.Colors.accentColor - : Theme.Colors.textInputUnfocusedStroke) + ? CoreAssets.accentColor.swiftUIColor + : CoreAssets.textInputUnfocusedStroke.swiftUIColor) ) .padding(.horizontal, 24) .padding(.bottom, 20) @@ -146,9 +147,7 @@ public struct SearchView: View { } } } - } - .navigationBarBackButtonHidden(true) - .navigationBarHidden(true) + }.hideNavigationBar() .onAppear { DispatchQueue.main.asyncAfter(deadline: .now()) { withAnimation(.easeIn(duration: 0.3)) { @@ -156,7 +155,7 @@ public struct SearchView: View { } } } - .background(Theme.Colors.background.ignoresSafeArea()) + .background(CoreAssets.background.swiftUIColor.ignoresSafeArea()) .addTapToEndEditing(isForced: true) } @@ -164,10 +163,10 @@ public struct SearchView: View { return VStack(alignment: .leading) { Text(DiscoveryLocalization.Search.title) .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Text(searchDescription(viewModel: viewModel)) .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) }.listRowBackground(Color.clear) } diff --git a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift index 1eb44a322..034c7c761 100644 --- a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift +++ b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift @@ -1613,12 +1613,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { perform?(`blocks`) } - open func deleteAllFiles() { - addInvocation(.m_deleteAllFiles) - let perform = methodPerformValue(.m_deleteAllFiles) as? () -> Void - perform?() - } - open func fileUrl(for blockId: String) -> URL? { addInvocation(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) let perform = methodPerformValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void @@ -1641,7 +1635,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_resumeDownloading case m_pauseDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) - case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -1673,8 +1666,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_deleteAllFiles, .m_deleteAllFiles): return .match - case (.m_fileUrl__for_blockId(let lhsBlockid), .m_fileUrl__for_blockId(let rhsBlockid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) @@ -1692,7 +1683,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return 0 case .m_pauseDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue - case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue } } @@ -1705,7 +1695,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return ".resumeDownloading()" case .m_pauseDownloading: return ".pauseDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" - case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" } } @@ -1792,7 +1781,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} - public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} } @@ -1821,9 +1809,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } - public static func deleteAllFiles(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_deleteAllFiles, performs: perform) - } public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } diff --git a/Discussion/Discussion.xcodeproj/project.pbxproj b/Discussion/Discussion.xcodeproj/project.pbxproj index 9a659e4a7..cfd6abaff 100644 --- a/Discussion/Discussion.xcodeproj/project.pbxproj +++ b/Discussion/Discussion.xcodeproj/project.pbxproj @@ -882,7 +882,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -916,7 +916,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1013,7 +1013,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1111,7 +1111,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1203,7 +1203,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1294,7 +1294,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1321,7 +1321,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1342,7 +1342,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1363,7 +1363,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1384,7 +1384,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1405,7 +1405,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1426,7 +1426,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1517,7 +1517,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1545,7 +1545,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1630,7 +1630,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1657,7 +1657,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; diff --git a/Discussion/Discussion/Data/Model/Data_CreatedComment.swift b/Discussion/Discussion/Data/Model/Data_CreatedComment.swift index b5ac48d50..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 ?? "", + 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 18530e784..a2bc621ec 100644 --- a/Discussion/Discussion/Data/Network/DiscussionRepository.swift +++ b/Discussion/Discussion/Data/Network/DiscussionRepository.swift @@ -36,11 +36,11 @@ public protocol DiscussionRepositoryProtocol { public class DiscussionRepository: DiscussionRepositoryProtocol { private let api: API - private let appStorage: CoreStorage + private let appStorage: AppStorage private let config: Config private let router: DiscussionRouter - public init(api: API, appStorage: CoreStorage, config: Config, router: DiscussionRouter) { + public init(api: API, appStorage: AppStorage, config: Config, router: DiscussionRouter) { self.api = api self.appStorage = appStorage self.config = config diff --git a/Discussion/Discussion/Presentation/CheckBoxView.swift b/Discussion/Discussion/Presentation/CheckBoxView.swift index 61af59732..bef86eacb 100644 --- a/Discussion/Discussion/Presentation/CheckBoxView.swift +++ b/Discussion/Discussion/Presentation/CheckBoxView.swift @@ -16,8 +16,8 @@ public struct CheckBoxView: View { HStack(spacing: 10) { Image(systemName: checked ? "checkmark.square.fill" : "square") .foregroundColor(checked - ? Theme.Colors.accentColor - : Theme.Colors.textPrimary) + ? CoreAssets.accentColor.swiftUIColor + : CoreAssets.textPrimary.swiftUIColor) Text(text) .font(Theme.Fonts.labelLarge) } diff --git a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift index bc2572e8d..d7b7ce10d 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift @@ -24,7 +24,7 @@ public class BaseResponsesViewModel { public var totalPages = 1 @Published public var itemsCount = 0 public var fetchInProgress = false - + var errorMessage: String? { didSet { withAnimation { @@ -44,16 +44,19 @@ public class BaseResponsesViewModel { internal let interactor: DiscussionInteractorProtocol internal let router: DiscussionRouter internal let config: Config + internal let storage: Core.AppStorage + internal let addPostSubject = CurrentValueSubject(nil) - init( - interactor: DiscussionInteractorProtocol, - router: DiscussionRouter, - config: Config + init(interactor: DiscussionInteractorProtocol, + router: DiscussionRouter, + config: Config, + storage: Core.AppStorage ) { self.interactor = interactor self.router = router self.config = config + self.storage = storage } @MainActor @@ -134,6 +137,7 @@ public class BaseResponsesViewModel { func addNewPost(_ post: Post) { var newPostWithAvatar = post + newPostWithAvatar.authorAvatar = storage.userProfile?.profileImage?.imageURLLarge ?? "" postComments?.comments.append(newPostWithAvatar) itemsCount += 1 } diff --git a/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift b/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift index f818590d2..d86eb4794 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift @@ -53,7 +53,7 @@ public struct CommentCell: View { .font(Theme.Fonts.titleSmall) Text(comment.postDate.dateToString(style: .lastPost)) .font(Theme.Fonts.labelSmall) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) } Spacer() Button(action: { @@ -67,8 +67,8 @@ public struct CommentCell: View { : DiscussionLocalization.Comment.report) .font(Theme.Fonts.labelMedium) }).foregroundColor(comment.abuseFlagged - ? Theme.Colors.alert - : Theme.Colors.textSecondary) + ? CoreAssets.alert.swiftUIColor + : CoreAssets.textSecondary.swiftUIColor) } Text(comment.postBodyHtml.hideHtmlTagsAndUrls()) .font(Theme.Fonts.bodyMedium) @@ -90,7 +90,7 @@ public struct CommentCell: View { Text(url.absoluteString) .multilineTextAlignment(.leading) } - }.foregroundColor(Theme.Colors.accentColor) + }.foregroundColor(CoreAssets.accentColor.swiftUIColor) .font(Theme.Fonts.bodyMedium) } } @@ -98,7 +98,7 @@ public struct CommentCell: View { LazyVStack { VStack {} .frame(height: 1) - .overlay(Theme.Colors.cardViewStroke) + .overlay(CoreAssets.cardViewStroke.swiftUIColor) .padding(.horizontal, 24) .onAppear { onFetchMore() @@ -115,8 +115,8 @@ public struct CommentCell: View { Text(DiscussionLocalization.votesCount(comment.votesCount)) .font(Theme.Fonts.labelLarge) }).foregroundColor(comment.voted - ? Theme.Colors.accentColor - : Theme.Colors.textSecondary) + ? CoreAssets.accentColor.swiftUIColor + : CoreAssets.textSecondary.swiftUIColor) Spacer() if addCommentAvailable { @@ -124,14 +124,14 @@ public struct CommentCell: View { Image(systemName: "message.fill") Text("\(comment.responsesCount)") Text(DiscussionLocalization.commentsCount(comment.responsesCount)) - }.foregroundColor(Theme.Colors.textSecondary) + }.foregroundColor(CoreAssets.textSecondary.swiftUIColor) .font(Theme.Fonts.labelLarge) } - }.foregroundColor(Theme.Colors.accentColor) + }.foregroundColor(CoreAssets.accentColor.swiftUIColor) .font(Theme.Fonts.labelMedium) }.cardStyle(top: leftLineEnabled ? 0 : 8, leftLineEnabled: leftLineEnabled, - bgColor: Theme.Colors.commentCellBackground) + bgColor: CoreAssets.commentCellBackground.swiftUIColor) .onTapGesture { if addCommentAvailable { onCommentsTap() diff --git a/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift b/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift index ab6aa3455..9becb7b63 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift @@ -48,7 +48,7 @@ public struct ParentCommentView: View { Text(comments.postDate .dateToString(style: .lastPost)) .font(Theme.Fonts.labelSmall) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) } Spacer() if isThread { @@ -60,8 +60,8 @@ public struct ParentCommentView: View { ? DiscussionLocalization.Comment.unfollow : DiscussionLocalization.Comment.follow) }).foregroundColor(comments.followed - ? Theme.Colors.accentColor - : Theme.Colors.textSecondary) + ? CoreAssets.accentColor.swiftUIColor + : CoreAssets.textSecondary.swiftUIColor) } }.padding(.top, 31) Text(comments.postTitle) @@ -85,7 +85,7 @@ public struct ParentCommentView: View { Text(url.absoluteString) .multilineTextAlignment(.leading) } - }.foregroundColor(Theme.Colors.accentColor) + }.foregroundColor(CoreAssets.accentColor.swiftUIColor) .font(Theme.Fonts.bodyMedium) } } @@ -100,8 +100,8 @@ public struct ParentCommentView: View { Text(DiscussionLocalization.votesCount(comments.votesCount)) .font(Theme.Fonts.labelLarge) }).foregroundColor(comments.voted - ? Theme.Colors.accentColor - : Theme.Colors.textSecondary) + ? CoreAssets.accentColor.swiftUIColor + : CoreAssets.textSecondary.swiftUIColor) Spacer() Button(action: { onReportTap() @@ -115,8 +115,8 @@ public struct ParentCommentView: View { }) } .accentColor(comments.abuseFlagged - ? Theme.Colors.snackbarErrorColor - : Theme.Colors.textSecondary) + ? CoreAssets.snackbarErrorColor.swiftUIColor + : CoreAssets.textSecondary.swiftUIColor) .font(Theme.Fonts.labelLarge) .padding(.top, 8) } @@ -124,7 +124,7 @@ public struct ParentCommentView: View { if isThread { Divider() .frame(height: 1) - .overlay(Theme.Colors.cardViewStroke) + .overlay(CoreAssets.cardViewStroke.swiftUIColor) .padding(.horizontal, 24) } } diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift index 69e666844..3df3ebdcf 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Kingfisher import Combine public struct ResponsesView: View { @@ -36,148 +37,151 @@ public struct ResponsesView: View { public var body: some View { ZStack(alignment: .top) { - // 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, + + // 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, onLikeTap: { Task { - await viewModel.vote( - id: comment.commentID, + if await viewModel.vote( + id: parentComment.commentID, isThread: false, - voted: comment.voted, - index: index - ) + voted: comments.voted, + index: nil + ) { + viewModel.sendThreadLikeState() + } } }, onReportTap: { Task { - await viewModel.flag( - id: comment.commentID, + if await viewModel.flag( + id: parentComment.commentID, isThread: false, - abuseFlagged: comment.abuseFlagged, - index: index - ) + abuseFlagged: comments.abuseFlagged, + index: nil + ) { + viewModel.sendThreadReportState() + } + } }, - onCommentsTap: {}, - onFetchMore: { - Task { - await viewModel.fetchMorePosts( - commentID: commentID, - parentComment: parentComment, - index: index - ) - } - } + onFollowTap: {} ) - .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: commentID + 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 + ) + } + } ) + .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 + ) + } + } + } + ) + } } } - } - .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) + .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 } - } else { - viewModel.alertMessage = DiscussionLocalization.Response.Alert.commentAdded - viewModel.showAlert = true - } - }) - .frame(maxWidth: .infinity, maxHeight: .infinity) - }.scrollAvoidKeyboard(dismissKeyboardByTap: true) - .padding(.top, 8) + }) + .frame(maxWidth: .infinity, maxHeight: .infinity) + }.scrollAvoidKeyboard(dismissKeyboardByTap: true) + } // MARK: - Error Alert if viewModel.showError { VStack { @@ -191,13 +195,9 @@ public struct ResponsesView: View { } } } - } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(title) - .edgesIgnoringSafeArea(.bottom) + }.edgesIgnoringSafeArea(.bottom) .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } @@ -210,6 +210,7 @@ struct ResponsesView_Previews: PreviewProvider { interactor: DiscussionInteractor(repository: DiscussionRepositoryMock()), router: DiscussionRouterMock(), config: ConfigMock(), + storage: .mock, threadStateSubject: .init(nil) ) let post = Post( diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift index 92555f692..1b8e0acc3 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift @@ -20,10 +20,11 @@ public class ResponsesViewModel: BaseResponsesViewModel, ObservableObject { interactor: DiscussionInteractorProtocol, router: DiscussionRouter, config: Config, + storage: Core.AppStorage, threadStateSubject: CurrentValueSubject ) { self.threadStateSubject = threadStateSubject - super.init(interactor: interactor, router: router, config: config) + super.init(interactor: interactor, router: router, config: config, storage: storage) } func generateCommentsResponses(comments: [UserComment], parentComment: Post) -> Post? { @@ -95,11 +96,7 @@ public class ResponsesViewModel: BaseResponsesViewModel, ObservableObject { .getCommentResponses(commentID: commentID, page: page) self.totalPages = pagination.numPages self.itemsCount = pagination.count - if page == 1 { - self.comments = comments - } else { - self.comments += comments - } + self.comments += comments postComments = generateCommentsResponses(comments: self.comments, parentComment: parentComment) return true } catch let error { diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index bdc5ae96a..acafd1bd9 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -7,11 +7,13 @@ import SwiftUI import Core +import WebKit +import Kingfisher public struct ThreadView: View { private var title: String - private let thread: UserThread + private let thread: UserThread? private var onBackTapped: (() -> Void) = {} @ObservedObject private var viewModel: ThreadViewModel @@ -25,159 +27,178 @@ public struct ThreadView: View { self.thread = thread self.title = thread.title self.viewModel = viewModel + Task { + await viewModel.getPosts(thread: thread, page: 1) + } } public var body: some View { ZStack(alignment: .top) { - // MARK: - Page Body - ScrollViewReader { scroll in - VStack { - ZStack(alignment: .top) { - RefreshableScrollViewCompat(action: { - _ = await viewModel.getPosts(thread: thread, page: 1) - }) { - VStack { - if let comments = viewModel.postComments { - ParentCommentView( - comments: comments, - isThread: true, - onLikeTap: { - Task { - if await viewModel.vote( - id: comments.threadID, - isThread: true, - voted: comments.voted, - index: nil - ) { - viewModel.sendPostLikedState() - } - } - }, - onReportTap: { - Task { - if await viewModel.flag( - id: comments.threadID, - isThread: true, - abuseFlagged: comments.abuseFlagged, - index: nil - ) { - viewModel.sendReportedState() - } - } - }, - onFollowTap: { - Task { - if await viewModel.followThread( - following: comments.followed, - threadID: comments.threadID - ) { - viewModel.sendPostFollowedState() - } - } - } - ) - - HStack { - Text("\(viewModel.itemsCount)") - Text(DiscussionLocalization.responsesCount(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: true, + // MARK: - Page name + VStack(alignment: .center) { + NavigationBar(title: title, + leftButtonAction: { + viewModel.router.back() + onBackTapped() + viewModel.sendUpdateUnreadState() + }) + + // MARK: - Page Body + ScrollViewReader { scroll in + VStack { + ZStack(alignment: .top) { + RefreshableScrollViewCompat(action: { + if let thread { + viewModel.comments = [] + _ = await viewModel.getPosts(thread: thread, page: 1) + } + }) { + VStack { + if let comments = viewModel.postComments { + ParentCommentView( + comments: comments, + isThread: true, onLikeTap: { Task { - await viewModel.vote( - id: comment.commentID, - isThread: false, - voted: comment.voted, - index: index - ) + if await viewModel.vote( + id: comments.threadID, + isThread: true, + voted: comments.voted, + index: nil + ) { + viewModel.sendPostLikedState() + } } }, onReportTap: { Task { - await viewModel.flag( - id: comment.commentID, - isThread: false, - abuseFlagged: comment.abuseFlagged, - index: index - ) + if await viewModel.flag( + id: comments.threadID, + isThread: true, + abuseFlagged: comments.abuseFlagged, + index: nil + ) { + viewModel.sendReportedState() + } } }, - onCommentsTap: { - viewModel.router.showComments( - commentID: comment.commentID, - parentComment: comment, - threadStateSubject: viewModel.threadStateSubject - ) - }, - onFetchMore: { + onFollowTap: { Task { - await viewModel.fetchMorePosts(thread: thread, - index: index) + if await viewModel.followThread( + following: comments.followed, + threadID: comments.threadID + ) { + viewModel.sendPostFollowedState() + } } } ) - .id(index) - } - if viewModel.nextPage <= viewModel.totalPages { - VStack(alignment: .center) { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 20) + + HStack { + Text("\(viewModel.itemsCount)") + Text(DiscussionLocalization.responsesCount(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: 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: { + viewModel.router.showComments( + commentID: comment.commentID, + parentComment: comment, + threadStateSubject: viewModel.threadStateSubject + ) + }, + onFetchMore: { + if let thread { + Task { + await viewModel.fetchMorePosts(thread: thread, + index: index) + } + } + } + ) + .id(index) } + if viewModel.nextPage <= viewModel.totalPages { + VStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 20) + } + } + Spacer(minLength: 84) } - Spacer(minLength: 84) + } + .frameLimit() + .onRightSwipeGesture { + viewModel.router.back() + onBackTapped() + viewModel.sendUpdateUnreadState() } } - .frameLimit() - .onRightSwipeGesture { - viewModel.router.back() - onBackTapped() - viewModel.sendUpdateUnreadState() - } - } - if !thread.closed { - FlexibleKeyboardInputView( - hint: DiscussionLocalization.Thread.addResponse, - sendText: { commentText in - if let threadID = viewModel.postComments?.threadID { - Task { - await viewModel.postComment( - threadID: threadID, - rawBody: commentText, - parentID: viewModel.postComments?.parentID - ) + if let thread { + if !thread.closed { + FlexibleKeyboardInputView( + hint: DiscussionLocalization.Thread.addResponse, + 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.sendPostRepliesCountState() - 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.Thread.Alert.commentAdded - viewModel.showAlert = true } - }) - .frame(maxWidth: .infinity, maxHeight: .infinity) - }.scrollAvoidKeyboard(dismissKeyboardByTap: true) + .onReceive(viewModel.addPostSubject, perform: { newComment in + guard let newComment else { return } + viewModel.sendPostRepliesCountState() + 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.Thread.Alert.commentAdded + viewModel.showAlert = true + } + }) + .frame(maxWidth: .infinity, maxHeight: .infinity) + }.scrollAvoidKeyboard(dismissKeyboardByTap: true) + } } - .padding(.top, 8) // MARK: - Error Alert if viewModel.showError { VStack { @@ -197,7 +218,7 @@ public struct ThreadView: View { VStack { Text(viewModel.alertMessage ?? "") .shadowCardStyle( - bgColor: Theme.Colors.accentColor, + bgColor: CoreAssets.accentColor.swiftUIColor, textColor: .white ) .padding(.top, 80) @@ -211,30 +232,19 @@ public struct ThreadView: View { } } } - } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(title) - .onFirstAppear { - Task { - await viewModel.getPosts(thread: thread, page: 1) - } - } - .onDisappear { - onBackTapped() - viewModel.sendUpdateUnreadState() - } - .edgesIgnoringSafeArea(.bottom) + }.edgesIgnoringSafeArea(.bottom) .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } private func reloadPage(onSuccess: @escaping () -> Void) { - Task { - if await viewModel.getPosts(thread: thread, - page: viewModel.nextPage-1) { onSuccess() } + if let thread { + Task { + if await viewModel.getPosts(thread: thread, + page: viewModel.nextPage-1) { onSuccess() } + } } } } @@ -266,6 +276,7 @@ struct CommentsView_Previews: PreviewProvider { let vm = ThreadViewModel(interactor: DiscussionInteractor.mock, router: DiscussionRouterMock(), config: ConfigMock(), + storage: .mock, postStateSubject: .init(nil)) ThreadView(thread: userThread, viewModel: vm) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index db10d8039..26d82ce7e 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -22,10 +22,11 @@ public class ThreadViewModel: BaseResponsesViewModel, ObservableObject { interactor: DiscussionInteractorProtocol, router: DiscussionRouter, config: Config, + storage: Core.AppStorage, postStateSubject: CurrentValueSubject ) { self.postStateSubject = postStateSubject - super.init(interactor: interactor, router: router, config: config) + super.init(interactor: interactor, router: router, config: config, storage: storage) cancellable = threadStateSubject .receive(on: RunLoop.main) @@ -135,22 +136,14 @@ public class ThreadViewModel: BaseResponsesViewModel, ObservableObject { .getQuestionComments(threadID: thread.id, page: page) self.totalPages = pagination.numPages self.itemsCount = pagination.count - if page == 1 { - self.comments = comments - } else { - self.comments += comments - } + 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 = pagination.numPages self.itemsCount = pagination.count - if page == 1 { - self.comments = comments - } else { - self.comments += comments - } + self.comments += comments postComments = generateComments(comments: self.comments, thread: thread) } fetchInProgress = false diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift index 64e0c487a..e21e8cf3c 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift @@ -36,13 +36,17 @@ public struct CreateNewThreadView: View { Task { await viewModel.getTopics(courseID: courseID) } - UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(Theme.Colors.accentColor) + UISegmentedControl.appearance().selectedSegmentTintColor = CoreAssets.accentColor.color UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected) } 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 { @@ -56,7 +60,7 @@ public struct CreateNewThreadView: View { HStack { Text(DiscussionLocalization.CreateThread.selectPostType) .font(Theme.Fonts.titleMedium) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) .padding(.top, 32) Spacer() } @@ -91,14 +95,14 @@ public struct CreateNewThreadView: View { Spacer() Image(systemName: "chevron.down") }.padding(.horizontal, 14) - .accentColor(Theme.Colors.textPrimary) + .accentColor(CoreAssets.textPrimary.swiftUIColor) .background(Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputStroke) + .fill(CoreAssets.textInputStroke.swiftUIColor) ) } } @@ -115,13 +119,13 @@ public struct CreateNewThreadView: View { .frame(height: 40) .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) .fill( - Theme.Colors.textInputStroke + CoreAssets.textInputStroke.swiftUIColor ) ) @@ -138,13 +142,13 @@ public struct CreateNewThreadView: View { .hideScrollContentBackground() .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) + .fill(CoreAssets.textInputBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) .fill( - Theme.Colors.textInputStroke + CoreAssets.textInputStroke.swiftUIColor ) ) @@ -182,13 +186,10 @@ public struct CreateNewThreadView: View { } }.scrollAvoidKeyboard(dismissKeyboardByTap: true) } - }.padding(.top, 8) + } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(DiscussionLocalization.CreateThread.newPost) .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift index 24d4335da..e7f925c02 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift @@ -25,15 +25,15 @@ public struct DiscussionSearchTopicsView: View { VStack(alignment: .center) { NavigationBar(title: DiscussionLocalization.search, leftButtonAction: { viewModel.router.backWithFade() }) - .padding(.bottom, -7) + HStack(spacing: 11) { Image(systemName: "magnifyingglass") .padding(.leading, 16) .padding(.top, -1) .foregroundColor( viewModel.isSearchActive - ? Theme.Colors.accentColor - : Theme.Colors.textPrimary + ? CoreAssets.accentColor.swiftUIColor + : CoreAssets.textPrimary.swiftUIColor ) TextField( @@ -51,7 +51,7 @@ public struct DiscussionSearchTopicsView: View { self.becomeFirstResponderRunOnce = true } }) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() if !viewModel.searchText.trimmingCharacters(in: .whitespaces).isEmpty { Button(action: { viewModel.searchText.removeAll() }, label: { @@ -61,24 +61,24 @@ public struct DiscussionSearchTopicsView: View { .frame(height: 24) .padding(.horizontal) }) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(CoreAssets.styledButtonText.swiftUIColor) } } -// .padding(.top, -7) + .padding(.top, 3) .frame(minHeight: 48) .frame(maxWidth: 532) .background( Theme.Shapes.textInputShape .fill(viewModel.isSearchActive - ? Theme.Colors.textInputBackground - : Theme.Colors.textInputUnfocusedBackground) + ? CoreAssets.textInputBackground.swiftUIColor + : CoreAssets.textInputUnfocusedBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) .fill(viewModel.isSearchActive - ? Theme.Colors.accentColor - : Theme.Colors.textInputUnfocusedStroke) + ? CoreAssets.accentColor.swiftUIColor + : CoreAssets.textInputUnfocusedStroke.swiftUIColor) ) .padding(.horizontal, 24) .padding(.bottom, 20) @@ -140,9 +140,7 @@ public struct DiscussionSearchTopicsView: View { } } } - } - .navigationBarBackButtonHidden(true) - .navigationBarHidden(true) + }.hideNavigationBar() .onAppear { DispatchQueue.main.asyncAfter(deadline: .now()) { withAnimation(.easeIn(duration: 0.3)) { @@ -150,7 +148,7 @@ public struct DiscussionSearchTopicsView: View { } } } - .background(Theme.Colors.background.ignoresSafeArea()) + .background(CoreAssets.background.swiftUIColor.ignoresSafeArea()) .addTapToEndEditing(isForced: true) } @@ -158,10 +156,10 @@ public struct DiscussionSearchTopicsView: View { return VStack(alignment: .leading) { Text(DiscussionLocalization.Search.title) .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Text(searchDescription(viewModel: viewModel)) .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) }.listRowBackground(Color.clear) } diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift index 722e1b9fd..acb81523d 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift @@ -11,38 +11,46 @@ import Core public struct DiscussionTopicsView: View { - @StateObject private var viewModel: DiscussionTopicsViewModel + @ObservedObject private var viewModel: DiscussionTopicsViewModel private let router: DiscussionRouter private let courseID: String public init(courseID: String, viewModel: DiscussionTopicsViewModel, router: DiscussionRouter) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.courseID = courseID + Task { + await viewModel.getTopics(courseID: courseID) + } self.router = router } public var body: some View { - ZStack(alignment: .center) { + ZStack(alignment: .top) { + + // MARK: - Page name VStack(alignment: .center) { + NavigationBar(title: DiscussionLocalization.title, + leftButtonAction: { router.back() }) + // MARK: - Search fake field HStack(spacing: 11) { Image(systemName: "magnifyingglass") .padding(.leading, 16) .padding(.top, 1) Text(DiscussionLocalization.Topics.search) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) Spacer() } .frame(maxWidth: 532) .frame(minHeight: 48) .background( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputUnfocusedBackground) + .fill(CoreAssets.textInputUnfocusedBackground.swiftUIColor) ) .overlay( Theme.Shapes.textInputShape .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputUnfocusedStroke) + .fill(CoreAssets.textInputUnfocusedStroke.swiftUIColor) ) .onTapGesture { viewModel.router.showDiscussionsSearch(courseID: courseID) @@ -54,14 +62,20 @@ public struct DiscussionTopicsView: View { VStack { ZStack(alignment: .top) { RefreshableScrollViewCompat(action: { - await viewModel.getTopics(courseID: self.courseID, withProgress: false) + await viewModel.getTopics(courseID: self.courseID, withProgress: isIOS14) }) { VStack { + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.horizontal) + .padding(.top, 200) + } + if let topics = viewModel.discussionTopics { HStack { Text(DiscussionLocalization.Topics.mainCategories) .font(Theme.Fonts.titleMedium) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) .padding(.horizontal, 24) .padding(.top, 40) Spacer() @@ -80,7 +94,7 @@ public struct DiscussionTopicsView: View { Spacer(minLength: 0) } .frame(maxWidth: .infinity) - }).cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground) + }).cardStyle(bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor) .padding(.trailing, -20) } if let followed = topics.first(where: { @@ -96,7 +110,7 @@ public struct DiscussionTopicsView: View { Spacer(minLength: 0) } .frame(maxWidth: .infinity) - }).cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground) + }).cardStyle(bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor) .padding(.leading, -20) } @@ -109,7 +123,7 @@ public struct DiscussionTopicsView: View { HStack { Text("\(topic.name):") .font(Theme.Fonts.titleMedium) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) Spacer() }.padding(.top, 32) .padding(.bottom, 8) @@ -128,28 +142,15 @@ public struct DiscussionTopicsView: View { Spacer(minLength: 84) } }.frameLimit() - .onRightSwipeGesture { - router.back() - } - + .onRightSwipeGesture { + router.back() + } } }.frame(maxWidth: .infinity) - }.padding(.top, 8) - if viewModel.isShowProgress { - ProgressBar(size: 40, lineWidth: 8) - .padding(.horizontal) - } - } - .onFirstAppear { - Task { - await viewModel.getTopics(courseID: courseID) } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(DiscussionLocalization.title) .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } @@ -200,10 +201,10 @@ public struct TopicCell: View { HStack { Text(topic.name) .font(Theme.Fonts.titleMedium) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Spacer() Image(systemName: "chevron.right") - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) } }) diff --git a/Discussion/Discussion/Presentation/Posts/PostsView.swift b/Discussion/Discussion/Presentation/Posts/PostsView.swift index dc7145a3a..e3936c3b3 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsView.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsView.swift @@ -14,6 +14,7 @@ public struct PostsView: View { @ObservedObject private var viewModel: PostsViewModel @State private var isShowProgress: Bool = true @State private var showingAlert = false + @State private var listAnimation: Animation? private let router: DiscussionRouter private let title: String private let currentBlockID: String @@ -31,6 +32,9 @@ public struct PostsView: View { self.viewModel.courseID = courseID self.viewModel.topics = topics viewModel.type = type + Task { + await viewModel.getPosts(courseID: courseID, pageNumber: 1, withProgress: true) + } } public init(courseID: String, router: DiscussionRouter, viewModel: PostsViewModel) { @@ -39,148 +43,161 @@ public struct PostsView: View { self.currentBlockID = "" self.router = router self.viewModel = viewModel + Task { + await viewModel.getPosts(courseID: courseID, pageNumber: 1, withProgress: true) + } self.showTopMenu = true self.viewModel.courseID = courseID } public var body: some View { ZStack(alignment: .top) { - // MARK: - Page Body - ScrollViewReader { scroll in - VStack { - ZStack(alignment: .top) { + + // MARK: - Page name + VStack(alignment: .center) { + if showTopMenu { + NavigationBar(title: title, + leftButtonAction: { router.back() }) + } + // MARK: - Page Body + ScrollViewReader { scroll in VStack { - VStack { - HStack { - Group { - Button(action: { - viewModel.generateButtons(type: .filter) - showingAlert = true - }, label: { - CoreAssets.filter.swiftUIImage - Text(viewModel.filterTitle.localizedValue) - }) - Spacer() - Button(action: { - viewModel.generateButtons(type: .sort) - showingAlert = true - }, label: { - CoreAssets.sort.swiftUIImage - Text(viewModel.sortTitle.localizedValue) - }) - }.foregroundColor(Theme.Colors.accentColor) - } .font(Theme.Fonts.labelMedium) - .padding(.horizontal, 24) - .padding(.vertical, 12) - .shadow(color: Theme.Colors.shadowColor, - radius: 12, y: 4) - .background( - Theme.Colors.background - ) - Divider().offset(y: -8) - } - .frameLimit() - RefreshableScrollViewCompat(action: { - viewModel.resetPosts() - _ = await viewModel.getPosts( - pageNumber: 1, - withProgress: false - ) - }) { - 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(Theme.Colors.textPrimary) - Spacer() - Button(action: { - router.createNewThread( - courseID: courseID, - selectedTopic: currentBlockID, - onPostCreated: { - reloadPage(onSuccess: { - withAnimation { - scroll.scrollTo(1) - } - }) + ZStack(alignment: .top) { + VStack { + HStack(alignment: .top) { + VStack { + HStack { + Group { + Button(action: { + listAnimation = .easeIn + viewModel.generateButtons(type: .filter) + showingAlert = true + }, label: { + CoreAssets.filter.swiftUIImage + Text(viewModel.filterTitle.localizedValue) }) - }, label: { - VStack { - CoreAssets.addComment.swiftUIImage - .font(Theme.Fonts.labelLarge) - .padding(6) - } - .foregroundColor(.white) + Spacer() + Button(action: { + listAnimation = .easeIn + viewModel.generateButtons(type: .sort) + showingAlert = true + }, label: { + CoreAssets.sort.swiftUIImage + Text(viewModel.sortTitle.localizedValue) + }) + }.foregroundColor(CoreAssets.accentColor.swiftUIColor) + } .font(Theme.Fonts.labelMedium) + .padding(.horizontal, 24) + .padding(.vertical, 12) + .shadow(color: CoreAssets.shadowColor.swiftUIColor, + radius: 12, y: 4) .background( - Circle() - .foregroundColor(Theme.Colors.accentColor) + CoreAssets.background.swiftUIColor ) - }) + Divider().offset(y: -8) } - .padding(.horizontal, 24) - - ForEach(posts, id: \.offset) { index, post in - PostCell(post: post).padding(24) - .id(UUID()) - .onAppear { - Task { - await viewModel.getPostsPagination( - index: index + }.frameLimit() + RefreshableScrollViewCompat(action: { + listAnimation = nil + viewModel.resetPosts() + _ = await viewModel.getPosts(courseID: courseID, + pageNumber: 1, + withProgress: isIOS14) + }) { + 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) } - } else { - if !viewModel.fetchInProgress { - VStack(spacing: 0) { - CoreAssets.discussionIcon.swiftUIImage - .renderingMode(.template) - .foregroundColor(Theme.Colors.textPrimary) - 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) + }.frameLimit() + .animation(listAnimation) + .onRightSwipeGesture { + router.back() } - } - } - }.frameLimit() - .animation(nil) - .onRightSwipeGesture { - router.back() } + }.frame(maxWidth: .infinity) } - }.frame(maxWidth: .infinity) - } - .padding(.top, 8) + } if viewModel.isShowProgress { VStack(alignment: .center) { ProgressBar(size: 40, lineWidth: 8) @@ -189,19 +206,8 @@ public struct PostsView: View { maxHeight: .infinity) } } - .onFirstAppear { - Task { - await viewModel.getPosts( - pageNumber: 1, - withProgress: true - ) - } - } - .navigationBarHidden(!showTopMenu) - .navigationBarBackButtonHidden(!showTopMenu) - .navigationTitle(title) .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) // MARK: - Action Sheet @@ -213,11 +219,11 @@ public struct PostsView: View { @MainActor private func reloadPage(onSuccess: @escaping () -> Void) { Task { + listAnimation = nil viewModel.resetPosts() - _ = await viewModel.getPosts( - pageNumber: 1, - withProgress: false - ) + _ = await viewModel.getPosts(courseID: courseID, + pageNumber: 1, + withProgress: isIOS14) onSuccess() } } @@ -283,21 +289,21 @@ public struct PostCell: View { Text(DiscussionLocalization.missedPostsCount(post.unreadCommentCount - 1)) } }.font(Theme.Fonts.labelSmall) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) Text(post.title) .multilineTextAlignment(.leading) .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) Text("\(DiscussionLocalization.Post.lastPost) \(post.lastPostDateFormatted)") .font(Theme.Fonts.labelSmall) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) HStack { CoreAssets.responses.swiftUIImage Text("\(post.replies - 1)") Text(DiscussionLocalization.responsesCount(post.replies - 1)) .font(Theme.Fonts.labelLarge) } - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) } }) } diff --git a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift index baad91ffc..7d93b50cd 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift @@ -28,7 +28,7 @@ public class PostsViewModel: ObservableObject { public var nextPage = 1 public var totalPages = 1 - @Published public private(set) var fetchInProgress = false + public private(set) var fetchInProgress = false public enum ButtonType { case sort @@ -43,7 +43,7 @@ public class PostsViewModel: ObservableObject { if let courseID { resetPosts() Task { - _ = await getPosts(pageNumber: 1) + _ = await getPosts(courseID: courseID, pageNumber: 1) } } } @@ -53,7 +53,7 @@ public class PostsViewModel: ObservableObject { if let courseID { resetPosts() Task { - _ = await getPosts(pageNumber: 1) + _ = await getPosts(courseID: courseID, pageNumber: 1) } } } @@ -109,6 +109,9 @@ public class PostsViewModel: ObservableObject { } public func resetPosts() { + filteredPosts = [] + discussionPosts = [] + threads.threads = [] nextPage = 1 totalPages = 1 } @@ -167,42 +170,87 @@ public class PostsViewModel: ObservableObject { } @MainActor - func getPostsPagination(index: Int, withProgress: Bool = true) async { - guard !fetchInProgress else { return } - if totalPages > 1, index >= filteredPosts.count - 3, nextPage <= totalPages { - _ = await getPosts( - pageNumber: self.nextPage, - withProgress: withProgress - ) + func getPostsPagination(courseID: String, index: Int, withProgress: Bool = true) async { + if !fetchInProgress { + if totalPages > 1 { + if index == filteredPosts.count - 3 { + if totalPages != 1 { + if nextPage <= totalPages { + _ = await getPosts(courseID: courseID, + pageNumber: self.nextPage, + withProgress: withProgress) + } + } + } + } } } @MainActor - public func getPosts(pageNumber: Int, withProgress: Bool = true) async -> Bool { + public func getPosts(courseID: String, pageNumber: Int, withProgress: Bool = true) async -> Bool { fetchInProgress = true isShowProgress = withProgress do { - if pageNumber == 1 { - threads.threads = try await getThreadsList(type: type, page: pageNumber) + switch type { + case .allPosts: + threads.threads += try await interactor + .getThreadsList(courseID: courseID, + type: .allPosts, + sort: sortTitle, + filter: filterTitle, + page: pageNumber).threads if threads.threads.indices.contains(0) { - totalPages = threads.threads[0].numPages - nextPage = 2 + self.totalPages = threads.threads[0].numPages + self.nextPage += 1 + fetchInProgress = false } - } else { - threads.threads += try await getThreadsList(type: type, page: pageNumber) + case .followingPosts: + threads.threads += try await interactor + .getThreadsList(courseID: courseID, + type: .followingPosts, + sort: sortTitle, + filter: filterTitle, + page: pageNumber).threads + if threads.threads.indices.contains(0) { + self.totalPages = threads.threads[0].numPages + self.nextPage += 1 + fetchInProgress = false + } + case .nonCourseTopics: + threads.threads += try await interactor + .getThreadsList(courseID: courseID, + type: .nonCourseTopics, + sort: sortTitle, + filter: filterTitle, + page: pageNumber).threads if threads.threads.indices.contains(0) { - totalPages = threads.threads[0].numPages - nextPage += 1 + self.totalPages = threads.threads[0].numPages + self.nextPage += 1 + fetchInProgress = false } + case .courseTopics(topicID: let topicID): + threads.threads += try await interactor + .getThreadsList(courseID: courseID, + type: .courseTopics(topicID: topicID), + sort: sortTitle, + filter: filterTitle, + page: pageNumber).threads + if threads.threads.indices.contains(0) { + self.totalPages = threads.threads[0].numPages + self.nextPage += 1 + fetchInProgress = false + } + case .none: + isShowProgress = false + return false } discussionPosts = generatePosts(threads: threads) filteredPosts = discussionPosts + self.filteredPosts = self.discussionPosts isShowProgress = false - fetchInProgress = false return true } catch let error { isShowProgress = false - fetchInProgress = false if error.isInternetError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection } else { @@ -212,18 +260,6 @@ public class PostsViewModel: ObservableObject { } } - @MainActor - private func getThreadsList(type: ThreadType, page: Int) async throws -> [UserThread] { - guard let courseID else { return [] } - return try await interactor.getThreadsList( - courseID: courseID, - type: type, - sort: sortTitle, - filter: filterTitle, - page: page - ).threads - } - private func updateUnreadCommentsCount(id: String) { var threads = threads.threads guard let index = threads.firstIndex(where: { $0.id == id }) else { return } diff --git a/Discussion/DiscussionTests/DiscussionMock.generated.swift b/Discussion/DiscussionTests/DiscussionMock.generated.swift index bcb9ac4df..6335fb469 100644 --- a/Discussion/DiscussionTests/DiscussionMock.generated.swift +++ b/Discussion/DiscussionTests/DiscussionMock.generated.swift @@ -2534,12 +2534,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { perform?(`blocks`) } - open func deleteAllFiles() { - addInvocation(.m_deleteAllFiles) - let perform = methodPerformValue(.m_deleteAllFiles) as? () -> Void - perform?() - } - open func fileUrl(for blockId: String) -> URL? { addInvocation(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) let perform = methodPerformValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void @@ -2562,7 +2556,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_resumeDownloading case m_pauseDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) - case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2594,8 +2587,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_deleteAllFiles, .m_deleteAllFiles): return .match - case (.m_fileUrl__for_blockId(let lhsBlockid), .m_fileUrl__for_blockId(let rhsBlockid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) @@ -2613,7 +2604,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return 0 case .m_pauseDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue - case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue } } @@ -2626,7 +2616,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return ".resumeDownloading()" case .m_pauseDownloading: return ".pauseDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" - case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" } } @@ -2713,7 +2702,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} - public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} } @@ -2742,9 +2730,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } - public static func deleteAllFiles(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_deleteAllFiles, performs: perform) - } public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } diff --git a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift index b6940c034..ee4da8ee4 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift @@ -54,7 +54,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false viewModel.postComments = post @@ -76,8 +76,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false viewModel.postComments = post @@ -100,8 +99,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false viewModel.postComments = post @@ -123,8 +121,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false viewModel.postComments = post @@ -148,8 +145,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -170,8 +166,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false Given(interactor, .voteThread(voted: .any, threadID: .any, willThrow: NSError())) @@ -190,8 +185,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false viewModel.postComments = post @@ -213,8 +207,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false viewModel.postComments = post @@ -236,8 +229,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -258,8 +250,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false Given(interactor, .flagThread(abuseFlagged: .any, threadID: .any, willThrow: NSError())) @@ -278,8 +269,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false viewModel.postComments = post @@ -301,8 +291,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -323,8 +312,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) - + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) var result = false Given(interactor, .followThread(following: .any, threadID: .any, willThrow: NSError())) @@ -343,7 +331,7 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config, storage: .mock) viewModel.postComments = post diff --git a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift index 724e99e32..d0909522b 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift @@ -212,6 +212,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: .mock, postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willProduce: {_ in})) @@ -241,6 +242,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: .mock, postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willProduce: {_ in})) @@ -270,6 +272,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: .mock, postStateSubject: .init(.readed(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -301,6 +304,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: .mock, postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willThrow: NSError())) @@ -328,6 +332,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: .mock, postStateSubject: .init(.readed(id: "1"))) let post = Post(authorName: "", @@ -368,6 +373,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: .mock, postStateSubject: .init(.readed(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -392,6 +398,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: .mock, postStateSubject: .init(.readed(id: "1"))) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willThrow: NSError()) ) @@ -415,6 +422,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: .mock, postStateSubject: .init(.readed(id: "1"))) viewModel.totalPages = 2 diff --git a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift index d079c8944..cd4dbab3c 100644 --- a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift @@ -108,30 +108,33 @@ final class PostViewModelTests: XCTestCase { var result = false let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) - viewModel.courseID = "1" viewModel.type = .allPosts Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willReturn: threads)) viewModel.type = .allPosts - result = await viewModel.getPosts(pageNumber: 1) + result = await viewModel.getPosts(courseID: "1", pageNumber: 1) XCTAssertTrue(result) result = false viewModel.type = .courseTopics(topicID: "") - result = await viewModel.getPosts(pageNumber: 1) + result = await viewModel.getPosts(courseID: "1", pageNumber: 1) XCTAssertTrue(result) result = false viewModel.type = .followingPosts - result = await viewModel.getPosts(pageNumber: 1) + result = await viewModel.getPosts(courseID: "1", pageNumber: 1) XCTAssertTrue(result) result = false viewModel.type = .nonCourseTopics - result = await viewModel.getPosts(pageNumber: 1) + result = await viewModel.getPosts(courseID: "1", pageNumber: 1) XCTAssertTrue(result) result = false + + viewModel.type = .none + result = await viewModel.getPosts(courseID: "1", pageNumber: 1) + XCTAssertFalse(result) Verify(interactor, 4, .getThreadsList(courseID: .value("1"), type: .any, sort: .any, filter: .any, page: .value(1))) @@ -151,9 +154,8 @@ final class PostViewModelTests: XCTestCase { Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willThrow: noInternetError)) - viewModel.courseID = "1" viewModel.type = .allPosts - result = await viewModel.getPosts(pageNumber: 1) + result = await viewModel.getPosts(courseID: "1", pageNumber: 1) Verify(interactor, 1, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any)) @@ -172,9 +174,8 @@ final class PostViewModelTests: XCTestCase { Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willThrow: NSError())) - viewModel.courseID = "1" viewModel.type = .allPosts - result = await viewModel.getPosts(pageNumber: 1) + result = await viewModel.getPosts(courseID: "1", pageNumber: 1) Verify(interactor, 1, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any)) @@ -192,10 +193,9 @@ final class PostViewModelTests: XCTestCase { Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willReturn: threads)) - viewModel.courseID = "1" viewModel.type = .allPosts viewModel.sortTitle = .mostActivity - _ = await viewModel.getPosts(pageNumber: 1) + _ = await viewModel.getPosts(courseID: "1", pageNumber: 1) XCTAssertTrue(viewModel.filteredPosts[0].title == "1") Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .value(.recentActivity), filter: .any, page: .any, @@ -203,7 +203,7 @@ final class PostViewModelTests: XCTestCase { viewModel.filterTitle = .unread viewModel.sortTitle = .recentActivity - _ = await viewModel.getPosts(pageNumber: 1) + _ = await viewModel.getPosts(courseID: "1", pageNumber: 1) XCTAssertTrue(viewModel.filteredPosts[0].title == "1") XCTAssertNotNil(viewModel.filteredPosts.first(where: {$0.unreadCommentCount == 4})) @@ -212,7 +212,7 @@ final class PostViewModelTests: XCTestCase { viewModel.filterTitle = .unanswered viewModel.sortTitle = .mostVotes - _ = await viewModel.getPosts(pageNumber: 1) + _ = await viewModel.getPosts(courseID: "1", pageNumber: 1) XCTAssertTrue(viewModel.filteredPosts[0].title == "1") XCTAssertNotNil(viewModel.filteredPosts.first(where: { $0.hasEndorsed })) diff --git a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift index 997364f10..183174526 100644 --- a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift @@ -108,6 +108,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: .mock, threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .getCommentResponses(commentID: .any, page: .any, @@ -135,6 +136,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: .mock, threadStateSubject: .init(.postAdded(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -161,6 +163,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: .mock, threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .getCommentResponses(commentID: .any, page: .any, willThrow: NSError())) @@ -184,6 +187,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: .mock, threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willReturn: post)) @@ -205,6 +209,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: .mock, threadStateSubject: .init(.postAdded(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -228,6 +233,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: .mock, threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willThrow: NSError())) @@ -249,6 +255,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: .mock, threadStateSubject: .init(.postAdded(id: "1"))) viewModel.totalPages = 2 diff --git a/LICENSE b/LICENSE index f49a4e16e..0ad25db4b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,661 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 8aecb5c84..a1df4ef51 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -7,20 +7,15 @@ objects = { /* Begin PBXBuildFile section */ - 020CA5D92AA0A25300970AAF /* AppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020CA5D82AA0A25300970AAF /* AppStorage.swift */; }; 0218196428F734FA00202564 /* Discussion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0218196328F734FA00202564 /* Discussion.framework */; }; 0218196528F734FA00202564 /* Discussion.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0218196328F734FA00202564 /* Discussion.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0219C67728F4347600D64452 /* Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0219C67628F4347600D64452 /* Course.framework */; }; 0219C67828F4347600D64452 /* Course.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0219C67628F4347600D64452 /* Course.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 025AD4AC2A6FB95C00AB8FA7 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */; }; + 02512FF2299534300024D438 /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02512FF1299534300024D438 /* CoreDataHandler.swift */; }; 025DE1A428DB4DAE0053E0F4 /* Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 025DE1A328DB4DAE0053E0F4 /* Profile.framework */; }; 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, ); }; }; - 0293A2032A6FCA590090A336 /* CorePersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0293A2022A6FCA590090A336 /* CorePersistence.swift */; }; - 0293A2052A6FCD430090A336 /* CoursePersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0293A2042A6FCD430090A336 /* CoursePersistence.swift */; }; - 0293A2072A6FCDA30090A336 /* DiscoveryPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0293A2062A6FCDA30090A336 /* DiscoveryPersistence.swift */; }; - 0293A2092A6FCDE50090A336 /* DashboardPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0293A2082A6FCDE50090A336 /* DashboardPersistence.swift */; }; 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 */; }; @@ -33,6 +28,7 @@ 0770DE1728D080A1006D8A5D /* RouteController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE1628D080A1006D8A5D /* RouteController.swift */; }; 0770DE1E28D084E8006D8A5D /* AppAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE1D28D084E8006D8A5D /* AppAssembly.swift */; }; 0770DE2028D0858A006D8A5D /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE1F28D0858A006D8A5D /* Router.swift */; }; + 0770DE2728D09209006D8A5D /* SwiftUIHostController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2628D09209006D8A5D /* SwiftUIHostController.swift */; }; 0770DE4B28D0A462006D8A5D /* Authorization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0770DE4A28D0A462006D8A5D /* Authorization.framework */; }; 0770DE4C28D0A462006D8A5D /* Authorization.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0770DE4A28D0A462006D8A5D /* Authorization.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0770DE5028D0A707006D8A5D /* NetworkAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE4F28D0A707006D8A5D /* NetworkAssembly.swift */; }; @@ -66,19 +62,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 020CA5D82AA0A25300970AAF /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = ""; }; 0218196328F734FA00202564 /* Discussion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discussion.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0219C67628F4347600D64452 /* Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Course.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 02450ABD29C35FF20094E2D0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; + 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 /* 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; }; - 0293A2022A6FCA590090A336 /* CorePersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePersistence.swift; sourceTree = ""; }; - 0293A2042A6FCD430090A336 /* CoursePersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursePersistence.swift; sourceTree = ""; }; - 0293A2062A6FCDA30090A336 /* DiscoveryPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPersistence.swift; sourceTree = ""; }; - 0293A2082A6FCDE50090A336 /* DashboardPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPersistence.swift; sourceTree = ""; }; 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 = ""; }; @@ -95,6 +86,7 @@ 0770DE1628D080A1006D8A5D /* RouteController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteController.swift; sourceTree = ""; }; 0770DE1D28D084E8006D8A5D /* AppAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAssembly.swift; sourceTree = ""; }; 0770DE1F28D0858A006D8A5D /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; + 0770DE2628D09209006D8A5D /* SwiftUIHostController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHostController.swift; sourceTree = ""; }; 0770DE4A28D0A462006D8A5D /* Authorization.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Authorization.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; @@ -131,19 +123,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0293A2012A6FC9E30090A336 /* Data */ = { - isa = PBXGroup; - children = ( - 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */, - 0293A2022A6FCA590090A336 /* CorePersistence.swift */, - 0293A2042A6FCD430090A336 /* CoursePersistence.swift */, - 0293A2062A6FCDA30090A336 /* DiscoveryPersistence.swift */, - 0293A2082A6FCDE50090A336 /* DashboardPersistence.swift */, - 020CA5D82AA0A25300970AAF /* AppStorage.swift */, - ); - path = Data; - sourceTree = ""; - }; 0727878C28D347B2002E9142 /* View */ = { isa = PBXGroup; children = ( @@ -189,7 +168,8 @@ 0770DE1F28D0858A006D8A5D /* Router.swift */, 0298DF2F2A4EF7230023A257 /* AnalyticsManager.swift */, 02F175302A4DA95B0019CD70 /* MainScreenAnalytics.swift */, - 0293A2012A6FC9E30090A336 /* Data */, + 02512FF1299534300024D438 /* CoreDataHandler.swift */, + 0770DE2628D09209006D8A5D /* SwiftUIHostController.swift */, 0727876C28D23312002E9142 /* Environment.swift */, 0727878C28D347B2002E9142 /* View */, 0770DE1A28D084BC006D8A5D /* DI */, @@ -373,21 +353,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 0293A2052A6FCD430090A336 /* CoursePersistence.swift in Sources */, - 020CA5D92AA0A25300970AAF /* AppStorage.swift in Sources */, 0298DF302A4EF7230023A257 /* AnalyticsManager.swift in Sources */, - 0293A2072A6FCDA30090A336 /* DiscoveryPersistence.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 */, - 0293A2032A6FCA590090A336 /* CorePersistence.swift in Sources */, 0770DE1E28D084E8006D8A5D /* AppAssembly.swift in Sources */, - 025AD4AC2A6FB95C00AB8FA7 /* DatabaseManager.swift in Sources */, 0770DE2028D0858A006D8A5D /* Router.swift in Sources */, 0727876D28D23312002E9142 /* Environment.swift in Sources */, - 0293A2092A6FCDE50090A336 /* DashboardPersistence.swift in Sources */, 0770DE1728D080A1006D8A5D /* RouteController.swift in Sources */, + 0770DE2728D09209006D8A5D /* SwiftUIHostController.swift in Sources */, 071009C928D1DB3F00344290 /* ScreenAssembly.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -511,7 +487,7 @@ INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -600,7 +576,7 @@ INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -695,7 +671,7 @@ INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -784,7 +760,7 @@ INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -933,7 +909,7 @@ INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -968,7 +944,7 @@ INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/OpenEdX/AppDelegate.swift b/OpenEdX/AppDelegate.swift index 3c3cad303..931f65420 100644 --- a/OpenEdX/AppDelegate.swift +++ b/OpenEdX/AppDelegate.swift @@ -90,8 +90,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { lastForceLogoutTime = Date().timeIntervalSince1970 - Container.shared.resolve(CoreStorage.self)?.clear() - Container.shared.resolve(DownloadManagerProtocol.self)?.deleteAllFiles() + Container.shared.resolve(AppStorage.self)?.clear() Container.shared.resolve(CoreDataHandlerProtocol.self)?.clear() window?.rootViewController = RouteController() } diff --git a/OpenEdX/CoreDataHandler.swift b/OpenEdX/CoreDataHandler.swift new file mode 100644 index 000000000..9746c78fd --- /dev/null +++ b/OpenEdX/CoreDataHandler.swift @@ -0,0 +1,35 @@ +// +// CoreDataHandler.swift +// OpenEdX +// +// Created by  Stepanok Ivan on 09.02.2023. +// + +import Foundation +import Core +import Dashboard +import Discovery +import Course + +class CoreDataHandler: CoreDataHandlerProtocol { + + private let dashboardPersistence: DashboardPersistenceProtocol + private let discoveryPersistence: DiscoveryPersistenceProtocol + private let coursePersistence: CoursePersistenceProtocol + + init( + dashboardPersistence: DashboardPersistenceProtocol, + discoveryPersistence: DiscoveryPersistenceProtocol, + coursePersistence: CoursePersistenceProtocol + ) { + self.dashboardPersistence = dashboardPersistence + self.discoveryPersistence = discoveryPersistence + self.coursePersistence = coursePersistence + } + + func clear() { + dashboardPersistence.clear() + discoveryPersistence.clear() + coursePersistence.clear() + } +} diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index edc74bd1f..2e1dabda0 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -70,21 +70,13 @@ class AppAssembly: Assembly { Connectivity() } - container.register(DatabaseManager.self) { _ in - DatabaseManager(databaseName: "Database") - }.inObjectScope(.container) - - container.register(CoreDataHandlerProtocol.self) { r in - r.resolve(DatabaseManager.self)! - }.inObjectScope(.container) - - container.register(CorePersistenceProtocol.self) { r in - CorePersistence(context: r.resolve(DatabaseManager.self)!.context) + container.register(CorePersistenceProtocol.self) { _ in + CorePersistence() }.inObjectScope(.container) container.register(DownloadManagerProtocol.self, factory: { r in DownloadManager(persistence: r.resolve(CorePersistenceProtocol.self)!, - appStorage: r.resolve(CoreStorage.self)!, + appStorage: r.resolve(AppStorage.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!) }).inObjectScope(.container) @@ -135,14 +127,6 @@ class AppAssembly: Assembly { ) }.inObjectScope(.container) - container.register(CoreStorage.self) { r in - r.resolve(AppStorage.self)! - }.inObjectScope(.container) - - container.register(ProfileStorage.self) { r in - r.resolve(AppStorage.self)! - }.inObjectScope(.container) - container.register(Validator.self) { _ in Validator() }.inObjectScope(.container) diff --git a/OpenEdX/DI/NetworkAssembly.swift b/OpenEdX/DI/NetworkAssembly.swift index 1f036a860..f609b5bc5 100644 --- a/OpenEdX/DI/NetworkAssembly.swift +++ b/OpenEdX/DI/NetworkAssembly.swift @@ -13,7 +13,7 @@ import Swinject class NetworkAssembly: Assembly { func assemble(container: Container) { container.register(RequestInterceptor.self) { r in - RequestInterceptor(config: r.resolve(Config.self)!, storage: r.resolve(CoreStorage.self)!) + RequestInterceptor(config: r.resolve(Config.self)!, appStorage: r.resolve(AppStorage.self)!) }.inObjectScope(.container) container.register(Alamofire.Session.self) { r in diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 1dd9ddddb..4e3d37103 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -19,11 +19,20 @@ import Discussion class ScreenAssembly: Assembly { func assemble(container: Container) { + // MARK: CoreDataHandler + container.register(CoreDataHandlerProtocol.self) { r in + CoreDataHandler( + dashboardPersistence: r.resolve(DashboardPersistenceProtocol.self)!, + discoveryPersistence: r.resolve(DiscoveryPersistenceProtocol.self)!, + coursePersistence: r.resolve(CoursePersistenceProtocol.self)! + ) + } + // MARK: Auth container.register(AuthRepositoryProtocol.self) { r in AuthRepository( api: r.resolve(API.self)!, - appStorage: r.resolve(CoreStorage.self)!, + appStorage: r.resolve(AppStorage.self)!, config: r.resolve(Config.self)! ) } @@ -62,14 +71,14 @@ class ScreenAssembly: Assembly { } // MARK: Discovery - container.register(DiscoveryPersistenceProtocol.self) { r in - DiscoveryPersistence(context: r.resolve(DatabaseManager.self)!.context) + container.register(DiscoveryPersistenceProtocol.self) { _ in + DiscoveryPersistence() } container.register(DiscoveryRepositoryProtocol.self) { r in DiscoveryRepository( api: r.resolve(API.self)!, - appStorage: r.resolve(CoreStorage.self)!, + appStorage: r.resolve(AppStorage.self)!, config: r.resolve(Config.self)!, persistence: r.resolve(DiscoveryPersistenceProtocol.self)! ) @@ -98,14 +107,14 @@ class ScreenAssembly: Assembly { } // MARK: Dashboard - container.register(DashboardPersistenceProtocol.self) { r in - DashboardPersistence(context: r.resolve(DatabaseManager.self)!.context) + container.register(DashboardPersistenceProtocol.self) { _ in + DashboardPersistence() } container.register(DashboardRepositoryProtocol.self) { r in DashboardRepository( api: r.resolve(API.self)!, - storage: r.resolve(CoreStorage.self)!, + appStorage: r.resolve(AppStorage.self)!, config: r.resolve(Config.self)!, persistence: r.resolve(DashboardPersistenceProtocol.self)! ) @@ -128,9 +137,8 @@ class ScreenAssembly: Assembly { container.register(ProfileRepositoryProtocol.self) { r in ProfileRepository( api: r.resolve(API.self)!, - storage: r.resolve(AppStorage.self)!, + appStorage: r.resolve(AppStorage.self)!, coreDataHandler: r.resolve(CoreDataHandlerProtocol.self)!, - downloadManager: r.resolve(DownloadManagerProtocol.self)!, config: r.resolve(Config.self)! ) } @@ -174,14 +182,14 @@ class ScreenAssembly: Assembly { } // MARK: Course - container.register(CoursePersistenceProtocol.self) { r in - CoursePersistence(context: r.resolve(DatabaseManager.self)!.context) + container.register(CoursePersistenceProtocol.self) { _ in + CoursePersistence() } container.register(CourseRepositoryProtocol.self) { r in CourseRepository( api: r.resolve(API.self)!, - appStorage: r.resolve(CoreStorage.self)!, + appStorage: r.resolve(AppStorage.self)!, config: r.resolve(Config.self)!, persistence: r.resolve(CoursePersistenceProtocol.self)! ) @@ -302,7 +310,7 @@ class ScreenAssembly: Assembly { container.register(DiscussionRepositoryProtocol.self) { r in DiscussionRepository( api: r.resolve(API.self)!, - appStorage: r.resolve(CoreStorage.self)!, + appStorage: r.resolve(AppStorage.self)!, config: r.resolve(Config.self)!, router: r.resolve(DiscussionRouter.self)! ) @@ -346,6 +354,7 @@ class ScreenAssembly: Assembly { interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, config: r.resolve(Config.self)!, + storage: r.resolve(AppStorage.self)!, postStateSubject: subject ) } @@ -355,6 +364,7 @@ class ScreenAssembly: Assembly { interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, config: r.resolve(Config.self)!, + storage: r.resolve(AppStorage.self)!, threadStateSubject: subject ) } diff --git a/OpenEdX/Data/DashboardPersistence.swift b/OpenEdX/Data/DashboardPersistence.swift deleted file mode 100644 index 06241c00f..000000000 --- a/OpenEdX/Data/DashboardPersistence.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// DashboardPersistence.swift -// OpenEdX -// -// Created by  Stepanok Ivan on 25.07.2023. -// - -import Dashboard -import Core -import Foundation -import CoreData - -public class DashboardPersistence: DashboardPersistenceProtocol { - - private var context: NSManagedObjectContext - - public init(context: NSManagedObjectContext) { - self.context = context - } - - public func loadMyCourses() throws -> [CourseItem] { - let result = try? context.fetch(CDDashboardCourse.fetchRequest()) - .map { CourseItem(name: $0.name ?? "", - org: $0.org ?? "", - shortDescription: $0.desc ?? "", - imageURL: $0.imageURL ?? "", - isActive: nil, - courseStart: $0.courseStart, - courseEnd: $0.courseEnd, - enrollmentStart: $0.enrollmentStart, - enrollmentEnd: $0.enrollmentEnd, - courseID: $0.courseID ?? "", - numPages: Int($0.numPages), - coursesCount: Int($0.courseCount))} - if let result, !result.isEmpty { - return result - } else { - throw NoCachedDataError() - } - } - - public func saveMyCourses(items: [CourseItem]) { - for item in items { - context.performAndWait { - let newItem = CDDashboardCourse(context: context) - context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - newItem.name = item.name - newItem.org = item.org - newItem.desc = item.shortDescription - newItem.imageURL = item.imageURL - newItem.courseStart = item.courseStart - newItem.courseEnd = item.courseEnd - newItem.enrollmentStart = item.enrollmentStart - newItem.enrollmentEnd = item.enrollmentEnd - newItem.numPages = Int32(item.numPages) - newItem.courseID = item.courseID - - do { - try context.save() - } catch { - print("⛔️⛔️⛔️⛔️⛔️", error) - } - } - } - } -} diff --git a/OpenEdX/Data/DatabaseManager.swift b/OpenEdX/Data/DatabaseManager.swift deleted file mode 100644 index b8f71b346..000000000 --- a/OpenEdX/Data/DatabaseManager.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// Persistence.swift -// OpenEdX -// -// Created by  Stepanok Ivan on 25.07.2023. -// - -import Foundation -import CoreData -import Core -import Discovery -import Dashboard -import Course - -class DatabaseManager: CoreDataHandlerProtocol { - - private let databaseName: String - - private let bundles: [Bundle] = [ - Bundle(for: CoreBundle.self), - Bundle(for: DiscoveryBundle.self), - Bundle(for: DashboardBundle.self), - Bundle(for: CourseBundle.self) - ] - - private lazy var persistentContainer: NSPersistentContainer = { - return createContainer() - }() - - public lazy var context: NSManagedObjectContext = { - return createContext() - }() - - init(databaseName: String) { - self.databaseName = databaseName - } - - private func createContainer() -> NSPersistentContainer { - let model = NSManagedObjectModel.mergedModel(from: bundles)! - let container = NSPersistentContainer(name: databaseName, managedObjectModel: model) - container.loadPersistentStores { _, error in - if let error = error { - print("Unresolved error \(error)") - fatalError() - } - } - - let description = NSPersistentStoreDescription() - description.shouldInferMappingModelAutomatically = true - description.shouldMigrateStoreAutomatically = true - container.persistentStoreDescriptions = [description] - - return container - } - - private func createContext() -> NSManagedObjectContext { - let context = persistentContainer.newBackgroundContext() - context.automaticallyMergesChangesFromParent = true - return context - } - - public func clear() { - let storeContainer = persistentContainer.persistentStoreCoordinator - for store in storeContainer.persistentStores { - do { - try storeContainer.destroyPersistentStore( - at: store.url!, - ofType: store.type, - options: nil - ) - } catch { - print("⛔️⛔️⛔️⛔️⛔️", error) - } - } - - // Re-create the persistent container - persistentContainer.loadPersistentStores { _, error in - if let error = error { - print("Unresolved error \(error)") - fatalError() - } - } - } -} diff --git a/OpenEdX/Data/DiscoveryPersistence.swift b/OpenEdX/Data/DiscoveryPersistence.swift deleted file mode 100644 index 7547d0c37..000000000 --- a/OpenEdX/Data/DiscoveryPersistence.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// DiscoveryPersistence.swift -// OpenEdX -// -// Created by  Stepanok Ivan on 25.07.2023. -// - -import Foundation -import Discovery -import CoreData -import Core - -public class DiscoveryPersistence: DiscoveryPersistenceProtocol { - - private var context: NSManagedObjectContext - - public init(context: NSManagedObjectContext) { - self.context = context - } - - public func loadDiscovery() throws -> [CourseItem] { - let result = try? context.fetch(CDDiscoveryCourse.fetchRequest()) - .map { CourseItem(name: $0.name ?? "", - org: $0.org ?? "", - shortDescription: $0.desc ?? "", - imageURL: $0.imageURL ?? "", - isActive: $0.isActive, - courseStart: $0.courseStart, - courseEnd: $0.courseEnd, - enrollmentStart: $0.enrollmentStart, - enrollmentEnd: $0.enrollmentEnd, - courseID: $0.courseID ?? "", - numPages: Int($0.numPages), - coursesCount: Int($0.courseCount))} - if let result, !result.isEmpty { - return result - } else { - throw NoCachedDataError() - } - } - - public func saveDiscovery(items: [CourseItem]) { - for item in items { - context.performAndWait { - let newItem = CDDiscoveryCourse(context: context) - context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - newItem.name = item.name - newItem.org = item.org - newItem.desc = item.shortDescription - newItem.imageURL = item.imageURL - if let isActive = item.isActive { - newItem.isActive = isActive - } - newItem.courseStart = item.courseStart - newItem.courseEnd = item.courseEnd - newItem.enrollmentStart = item.enrollmentStart - newItem.enrollmentEnd = item.enrollmentEnd - newItem.numPages = Int32(item.numPages) - newItem.courseID = item.courseID - - do { - try context.save() - } catch { - print("⛔️⛔️⛔️⛔️⛔️", error) - } - } - } - } -} diff --git a/OpenEdX/RouteController.swift b/OpenEdX/RouteController.swift index 1aa036dc1..a4264ca0a 100644 --- a/OpenEdX/RouteController.swift +++ b/OpenEdX/RouteController.swift @@ -6,7 +6,6 @@ // import UIKit -import SwiftUI import Core import Authorization @@ -16,8 +15,8 @@ class RouteController: UIViewController { diContainer.resolve(UINavigationController.self)! }() - private lazy var appStorage: CoreStorage = { - diContainer.resolve(CoreStorage.self)! + private lazy var appStorage: AppStorage = { + diContainer.resolve(AppStorage.self)! }() private lazy var analytics: AuthorizationAnalytics = { @@ -40,15 +39,15 @@ class RouteController: UIViewController { } private func showAuthorization() { - let controller = UIHostingController( - rootView: SignInView(viewModel: diContainer.resolve(SignInViewModel.self)!) + let controller = SwiftUIHostController( + view: SignInView(viewModel: diContainer.resolve(SignInViewModel.self)!) ) navigation.viewControllers = [controller] present(navigation, animated: false) } private func showMainScreen() { - let controller = UIHostingController(rootView: MainScreenView()) + let controller = SwiftUIHostController(view: MainScreenView()) navigation.viewControllers = [controller] present(navigation, animated: false) } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 9be51948e..0e7e332cb 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -17,7 +17,7 @@ import Discovery import Dashboard import Profile import Combine - + public class Router: AuthorizationRouter, DiscoveryRouter, ProfileRouter, @@ -57,14 +57,13 @@ public class Router: AuthorizationRouter, } public func showMainScreen() { - showToolBar() - let controller = UIHostingController(rootView: MainScreenView()) + let controller = SwiftUIHostController(view: MainScreenView()) navigationController.setViewControllers([controller], animated: true) } public func showLoginScreen() { let view = SignInView(viewModel: Container.shared.resolve(SignInViewModel.self)!) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.setViewControllers([controller], animated: false) } @@ -123,13 +122,13 @@ public class Router: AuthorizationRouter, public func showRegisterScreen() { let view = SignUpView(viewModel: Container.shared.resolve(SignUpViewModel.self)!) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } public func showForgotPasswordScreen() { let view = ResetPasswordView(viewModel: Container.shared.resolve(ResetPasswordViewModel.self)!) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -139,7 +138,7 @@ public class Router: AuthorizationRouter, courseID: courseID, title: title ) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -147,7 +146,7 @@ public class Router: AuthorizationRouter, let viewModel = Container.shared.resolve(SearchViewModel.self)! let view = SearchView(viewModel: viewModel) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushFade(viewController: controller) } @@ -155,7 +154,7 @@ public class Router: AuthorizationRouter, let viewModel = Container.shared.resolve(DiscussionSearchTopicsViewModel.self, argument: courseID)! let view = DiscussionSearchTopicsView(viewModel: viewModel) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushFade(viewController: controller) } @@ -180,7 +179,7 @@ public class Router: AuthorizationRouter, courseID: courseID, viewModel: viewModel ) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -207,7 +206,7 @@ public class Router: AuthorizationRouter, title: title ) - let controller = UIHostingController(rootView: screensView) + let controller = SwiftUIHostController(view: screensView) navigationController.pushViewController(controller, animated: true) } @@ -223,7 +222,7 @@ public class Router: AuthorizationRouter, router: router, cssInjector: cssInjector ) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -248,7 +247,7 @@ public class Router: AuthorizationRouter, verticalIndex )! let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -276,7 +275,7 @@ public class Router: AuthorizationRouter, courseID: courseID, viewModel: vmVertical ) - let controllerVertical = UIHostingController(rootView: viewVertical) + let controllerVertical = SwiftUIHostController(view: viewVertical) let viewModel = Container.shared.resolve( CourseUnitViewModel.self, @@ -289,7 +288,7 @@ public class Router: AuthorizationRouter, verticalIndex )! let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName) - let controllerUnit = UIHostingController(rootView: view) + let controllerUnit = SwiftUIHostController(view: view) var controllers = navigationController.viewControllers controllers.removeLast(2) controllers.append(contentsOf: [controllerVertical, controllerUnit]) @@ -308,14 +307,14 @@ public class Router: AuthorizationRouter, viewModel: viewModel, router: router ) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } public func showThread(thread: UserThread, postStateSubject: CurrentValueSubject) { let viewModel = Container.shared.resolve(ThreadViewModel.self, argument: postStateSubject)! let view = ThreadView(thread: thread, viewModel: viewModel) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -332,7 +331,7 @@ public class Router: AuthorizationRouter, router: router, parentComment: parentComment ) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -348,7 +347,7 @@ public class Router: AuthorizationRouter, courseID: courseID, onPostCreated: onPostCreated ) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -363,20 +362,20 @@ public class Router: AuthorizationRouter, avatar: avatar, profileDidEdit: profileDidEdit ) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } public func showSettings() { let viewModel = Container.shared.resolve(SettingsViewModel.self)! let view = SettingsView(viewModel: viewModel) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } public func showVideoQualityView(viewModel: SettingsViewModel) { let view = VideoQualityView(viewModel: viewModel) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -392,7 +391,7 @@ public class Router: AuthorizationRouter, let viewModel = Container.shared.resolve(DeleteAccountViewModel.self)! let view = DeleteAccountView(viewModel: viewModel) - let controller = UIHostingController(rootView: view) + let controller = SwiftUIHostController(view: view) navigationController.pushViewController(controller, animated: true) } @@ -404,8 +403,4 @@ public class Router: AuthorizationRouter, hosting.modalPresentationStyle = .overFullScreen return hosting } - - private func showToolBar() { - self.navigationController.setNavigationBarHidden(false, animated: false) - } } diff --git a/OpenEdX/SwiftUIHostController.swift b/OpenEdX/SwiftUIHostController.swift new file mode 100644 index 000000000..922edce15 --- /dev/null +++ b/OpenEdX/SwiftUIHostController.swift @@ -0,0 +1,50 @@ +// +// SwiftUIHostController.swift +// OpenEdX +// +// Created by Vladimir Chekyrta on 13.09.2022. +// + +import UIKit +import SwiftUI +import Core + +public class SwiftUIHostController: UIViewController { + + private var innerView: InnerView + + public init(view: InnerView) { + self.innerView = view + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + let childView = UIHostingController(rootView: innerView) + addChild(childView) + view.addSubview(childView.view) + childView.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + childView.view.centerXAnchor.constraint(equalTo: view.centerXAnchor), + childView.view.centerYAnchor.constraint(equalTo: view.centerYAnchor), + childView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), + childView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), + childView.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 0), + childView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0) + ]) + childView.didMove(toParent: self) + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + DispatchQueue.main.async { + self.navigationController?.setNavigationBarHidden(true, animated: false) + } + } + +} diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index c27d7ea3f..fa57435d4 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -16,7 +16,6 @@ import SwiftUIIntrospect struct MainScreenView: View { @State private var selection: MainTab = .discovery - @State private var settingsTapped: Bool = false enum MainTab { case discovery @@ -25,13 +24,13 @@ struct MainScreenView: View { case profile } - private let analytics = Container.shared.resolve(MainScreenAnalytics.self)! + let analytics = Container.shared.resolve(MainScreenAnalytics.self)! init() { UITabBar.appearance().isTranslucent = false - UITabBar.appearance().barTintColor = UIColor(Theme.Colors.textInputUnfocusedBackground) - UITabBar.appearance().backgroundColor = UIColor(Theme.Colors.textInputUnfocusedBackground) - UITabBar.appearance().unselectedItemTintColor = UIColor(Theme.Colors.textSecondary) + UITabBar.appearance().barTintColor = CoreAssets.textInputUnfocusedBackground.color + UITabBar.appearance().backgroundColor = CoreAssets.textInputUnfocusedBackground.color + UITabBar.appearance().unselectedItemTintColor = CoreAssets.textSecondary.color } var body: some View { @@ -45,7 +44,8 @@ struct MainScreenView: View { Text(CoreLocalization.Mainscreen.discovery) } .tag(MainTab.discovery) - + .hideNavigationBar() + VStack { DashboardView( viewModel: Container.shared.resolve(DashboardViewModel.self)!, @@ -57,6 +57,7 @@ struct MainScreenView: View { Text(CoreLocalization.Mainscreen.dashboard) } .tag(MainTab.dashboard) + .hideNavigationBar() VStack { Text(CoreLocalization.Mainscreen.inDeveloping) @@ -66,10 +67,11 @@ struct MainScreenView: View { Text(CoreLocalization.Mainscreen.programs) } .tag(MainTab.programs) - + .hideNavigationBar() + VStack { ProfileView( - viewModel: Container.shared.resolve(ProfileViewModel.self)!, settingsTapped: $settingsTapped + viewModel: Container.shared.resolve(ProfileViewModel.self)! ) } .tabItem { @@ -77,49 +79,20 @@ struct MainScreenView: View { Text(CoreLocalization.Mainscreen.profile) } .tag(MainTab.profile) + .hideNavigationBar() } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(titleBar()) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing, content: { - if selection == .profile { - Button(action: { - settingsTapped.toggle() - }, label: { - CoreAssets.edit.swiftUIImage - .foregroundColor(Theme.Colors.textPrimary) - }) - } else { - VStack {} + .onChange(of: selection, perform: { selection in + switch selection { + case .discovery: + analytics.mainDiscoveryTabClicked() + case .dashboard: + analytics.mainDashboardTabClicked() + case .programs: + analytics.mainProgramsTabClicked() + case .profile: + analytics.mainProfileTabClicked() } }) - } - .onChange(of: selection, perform: { selection in - switch selection { - case .discovery: - analytics.mainDiscoveryTabClicked() - case .dashboard: - analytics.mainDashboardTabClicked() - case .programs: - analytics.mainProgramsTabClicked() - case .profile: - analytics.mainProfileTabClicked() - } - }) - } - - private func titleBar() -> String { - switch selection { - case .discovery: - return DiscoveryLocalization.title - case .dashboard: - return DashboardLocalization.title - case .programs: - return CoreLocalization.Mainscreen.programs - case .profile: - return ProfileLocalization.title - } } struct MainScreenView_Previews: PreviewProvider { diff --git a/OpenEdX/uk.lproj/languages.json b/OpenEdX/uk.lproj/languages.json index 971a7c53b..5c07f7b8c 100644 --- a/OpenEdX/uk.lproj/languages.json +++ b/OpenEdX/uk.lproj/languages.json @@ -1,6 +1,6 @@ [ { - "aa": "Афарська" + "аа": "Афарська" }, { "ab": "Абхазька" diff --git a/Profile/Data/ProfileStorage.swift b/Profile/Data/ProfileStorage.swift deleted file mode 100644 index 2770f6060..000000000 --- a/Profile/Data/ProfileStorage.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ProfileStorage.swift -// Profile -// -// Created by  Stepanok Ivan on 30.08.2023. -// - -import Foundation -import Core - -public protocol ProfileStorage { - var userProfile: DataLayer.UserProfile? {get set} -} - -#if DEBUG -public class ProfileStorageMock: ProfileStorage { - - public var userProfile: DataLayer.UserProfile? - - public init() {} -} -#endif diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index 3ba3e0531..c92a7d9d7 100644 --- a/Profile/Profile.xcodeproj/project.pbxproj +++ b/Profile/Profile.xcodeproj/project.pbxproj @@ -29,7 +29,6 @@ 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 */; }; - 02B089432A9F832200754BD4 /* ProfileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B089422A9F832200754BD4 /* ProfileStorage.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 */; }; @@ -71,7 +70,6 @@ 02A9A91A2978194A00B55797 /* ProfileTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ProfileTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; - 02B089422A9F832200754BD4 /* ProfileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStorage.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 = ""; }; @@ -140,7 +138,6 @@ 020F834028DB4CCD0062FA70 = { isa = PBXGroup; children = ( - 02B089412A9F830D00754BD4 /* Data */, 021D925B28DDADBD00ACC565 /* swiftgen.yml */, 020F834C28DB4CCD0062FA70 /* Profile */, 02A9A91B2978194A00B55797 /* ProfileTests */, @@ -279,14 +276,6 @@ path = ProfileTests; sourceTree = ""; }; - 02B089412A9F830D00754BD4 /* Data */ = { - isa = PBXGroup; - children = ( - 02B089422A9F832200754BD4 /* ProfileStorage.swift */, - ); - path = Data; - sourceTree = ""; - }; 0766DFD3299AD9D800EBEF6A /* Presentation */ = { isa = PBXGroup; children = ( @@ -555,7 +544,6 @@ 021D925528DC92F800ACC565 /* ProfileInteractor.swift in Sources */, 029301DA2938948500E99AB8 /* ProfileType.swift in Sources */, 0259104429C39C9E004B5A55 /* SettingsView.swift in Sources */, - 02B089432A9F832200754BD4 /* ProfileStorage.swift in Sources */, 021D925228DC918D00ACC565 /* ProfileViewModel.swift in Sources */, 0248F9B128DDB09D0041327E /* Strings.swift in Sources */, 020306CA2932B14D000949EA /* EditProfileViewModel.swift in Sources */, @@ -737,7 +725,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -772,7 +760,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -869,7 +857,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -967,7 +955,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1059,7 +1047,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1150,7 +1138,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1177,7 +1165,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1198,7 +1186,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1219,7 +1207,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1240,7 +1228,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1261,7 +1249,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1282,7 +1270,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1373,7 +1361,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1401,7 +1389,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1486,7 +1474,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1513,7 +1501,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; diff --git a/Profile/Profile/Data/ProfileRepository.swift b/Profile/Profile/Data/ProfileRepository.swift index 9c95c9a7a..0d3d52798 100644 --- a/Profile/Profile/Data/ProfileRepository.swift +++ b/Profile/Profile/Data/ProfileRepository.swift @@ -26,36 +26,28 @@ public protocol ProfileRepositoryProtocol { public class ProfileRepository: ProfileRepositoryProtocol { private let api: API - private var storage: CoreStorage & ProfileStorage - private let downloadManager: DownloadManagerProtocol + private let appStorage: AppStorage private let coreDataHandler: CoreDataHandlerProtocol private let config: Config - public init( - api: API, - storage: CoreStorage & ProfileStorage, - coreDataHandler: CoreDataHandlerProtocol, - downloadManager: DownloadManagerProtocol, - config: Config - ) { + public init(api: API, appStorage: AppStorage, coreDataHandler: CoreDataHandlerProtocol, config: Config) { self.api = api - self.storage = storage + self.appStorage = appStorage self.coreDataHandler = coreDataHandler - self.downloadManager = downloadManager self.config = config } public func getMyProfile() async throws -> UserProfile { let user = try await api.requestData( - ProfileEndpoint.getUserProfile(username: storage.user?.username ?? "") + ProfileEndpoint.getUserProfile(username: appStorage.user?.username ?? "") ).mapResponse(DataLayer.UserProfile.self) - storage.userProfile = user + appStorage.userProfile = user return user.domain } public func getMyProfileOffline() throws -> UserProfile { - if let user = storage.userProfile { + if let user = appStorage.userProfile { return user.domain } else { throw NoCachedDataError() @@ -63,12 +55,11 @@ public class ProfileRepository: ProfileRepositoryProtocol { } public func logOut() async throws { - guard let refreshToken = storage.refreshToken else { return } + guard let refreshToken = appStorage.refreshToken else { return } _ = try await api.request( ProfileEndpoint.logOut(refreshToken: refreshToken, clientID: config.oAuthClientId) ) - storage.clear() - downloadManager.deleteAllFiles() + appStorage.clear() coreDataHandler.clear() } @@ -110,23 +101,23 @@ public class ProfileRepository: ProfileRepositoryProtocol { } public func uploadProfilePicture(pictureData: Data) async throws { - let response = try await api.request( - ProfileEndpoint.uploadProfilePicture(username: storage.user?.username ?? "", - pictureData: pictureData)) + let response = try await api.request( + ProfileEndpoint.uploadProfilePicture(username: appStorage.user?.username ?? "", + pictureData: pictureData)) if response.statusCode != 204 { throw APIError.uploadError } } public func deleteProfilePicture() async throws -> Bool { - let response = try await api.request( - ProfileEndpoint.deleteProfilePicture(username: storage.user?.username ?? "")) + let response = try await api.request( + ProfileEndpoint.deleteProfilePicture(username: appStorage.user?.username ?? "")) return response.statusCode == 204 } public func updateUserProfile(parameters: [String: Any]) async throws -> UserProfile { let response = try await api.requestData( - ProfileEndpoint.updateUserProfile(username: storage.user?.username ?? "", + ProfileEndpoint.updateUserProfile(username: appStorage.user?.username ?? "", parameters: parameters)) .mapResponse(DataLayer.UserProfile.self).domain return response @@ -138,7 +129,7 @@ public class ProfileRepository: ProfileRepositoryProtocol { } public func getSettings() -> UserSettings { - if let userSettings = storage.userSettings { + if let userSettings = appStorage.userSettings { return userSettings } else { return UserSettings(wifiOnly: true, downloadQuality: VideoQuality.auto) @@ -146,7 +137,7 @@ public class ProfileRepository: ProfileRepositoryProtocol { } public func saveSettings(_ settings: UserSettings) { - storage.userSettings = settings + appStorage.userSettings = settings } } diff --git a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift index 88c5546a2..bef8c06c9 100644 --- a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift +++ b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift @@ -19,104 +19,111 @@ public struct DeleteAccountView: View { public var body: some View { ZStack(alignment: .top) { - // MARK: - Page Body - ScrollView { - VStack { - Group { - CoreAssets.deleteAccount.swiftUIImage - .padding(.top, 50) - Text(ProfileLocalization.DeleteAccount.areYouSure) - .foregroundColor(Theme.Colors.textPrimary) - + Text(ProfileLocalization.DeleteAccount.wantToDelete) - .foregroundColor(Theme.Colors.alert) - }.multilineTextAlignment(.center) - .font(Theme.Fonts.headlineSmall) - - Text(ProfileLocalization.DeleteAccount.description) - .foregroundColor(Theme.Colors.textSecondary) - .font(Theme.Fonts.labelLarge) - .multilineTextAlignment(.center) - .padding(.top, 16) - - // MARK: Password - Group { - Text(ProfileLocalization.DeleteAccount.password) - .foregroundColor(Theme.Colors.textSecondary) + + // MARK: - Page name + VStack(alignment: .center) { + NavigationBar( + title: ProfileLocalization.DeleteAccount.title, + leftButtonAction: { viewModel.router.back() } + ) + + .frameLimit() + + // MARK: - Page Body + ScrollView { + VStack { + Group { + CoreAssets.deleteAccount.swiftUIImage + .padding(.top, 50) + Text(ProfileLocalization.DeleteAccount.areYouSure) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) + + Text(ProfileLocalization.DeleteAccount.wantToDelete) + .foregroundColor(CoreAssets.alert.swiftUIColor) + }.multilineTextAlignment(.center) + .font(Theme.Fonts.headlineSmall) + + Text(ProfileLocalization.DeleteAccount.description) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) .font(Theme.Fonts.labelLarge) - .multilineTextAlignment(.leading) + .multilineTextAlignment(.center) .padding(.top, 16) - HStack(spacing: 11) { - SecureField(ProfileLocalization.DeleteAccount.passwordDescription, - text: $viewModel.password) + // MARK: Password + Group { + Text(ProfileLocalization.DeleteAccount.password) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) + .font(Theme.Fonts.labelLarge) + .multilineTextAlignment(.leading) + .padding(.top, 16) + + HStack(spacing: 11) { + SecureField(ProfileLocalization.DeleteAccount.passwordDescription, + text: $viewModel.password) + .font(Theme.Fonts.labelLarge) + .foregroundColor(CoreAssets.textPrimary.swiftUIColor) + } + .padding(.horizontal, 14) + .frame(minHeight: 48) + .frame(maxWidth: .infinity) + .background( + Theme.Shapes.textInputShape + .fill(CoreAssets.textInputBackground.swiftUIColor) + ) + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill(CoreAssets.textInputUnfocusedStroke.swiftUIColor) + ) + Text(viewModel.incorrectPassword + ? ProfileLocalization.DeleteAccount.incorrectPassword + : " ") + .foregroundColor(CoreAssets.alert.swiftUIColor) .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.leading) + .padding(.top, 0) + .shake($viewModel.incorrectPassword, + onCompletion: { viewModel.incorrectPassword.toggle() }) + + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + + // MARK: Comfirmation button + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 20) + .padding(.horizontal) + } else { + StyledButton(ProfileLocalization.DeleteAccount.comfirm, action: { + Task { + try await viewModel.deleteAccount(password: viewModel.password) + } + }, color: CoreAssets.alert.swiftUIColor, + isActive: viewModel.password.count >= 2) + .padding(.top, 18) } - .padding(.horizontal, 14) - .frame(minHeight: 48) - .frame(maxWidth: .infinity) - .background( - Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputUnfocusedStroke) - ) - Text(viewModel.incorrectPassword - ? ProfileLocalization.DeleteAccount.incorrectPassword - : " ") - .foregroundColor(Theme.Colors.alert) - .font(Theme.Fonts.labelLarge) - .multilineTextAlignment(.leading) - .padding(.top, 0) - .shake($viewModel.incorrectPassword, - onCompletion: { viewModel.incorrectPassword.toggle() }) - }.frame(minWidth: 0, - maxWidth: .infinity, - alignment: .topLeading) - - // MARK: Comfirmation button - if viewModel.isShowProgress { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 20) - .padding(.horizontal) - } else { - StyledButton(ProfileLocalization.DeleteAccount.comfirm, action: { - Task { - try await viewModel.deleteAccount(password: viewModel.password) + // MARK: Back to profile + Button(action: { + viewModel.router.back() + }, label: { + HStack(spacing: 9) { + CoreAssets.arrowRight16.swiftUIImage.renderingMode(.template) + .rotationEffect(Angle(degrees: 180)) + Text(ProfileLocalization.DeleteAccount.backToProfile) + .font(Theme.Fonts.labelLarge) } - }, color: Theme.Colors.alert, - isActive: viewModel.password.count >= 2) - .padding(.top, 18) + }) + .padding(.top, 35) + } - - // MARK: Back to profile - Button(action: { - viewModel.router.back() - }, label: { - HStack(spacing: 9) { - CoreAssets.arrowRight16.swiftUIImage.renderingMode(.template) - .rotationEffect(Angle(degrees: 180)) - Text(ProfileLocalization.DeleteAccount.backToProfile) - .font(Theme.Fonts.labelLarge) - } - }) - .padding(.top, 35) - - } - }.padding(.horizontal, 24) - .frame(minHeight: 0, - maxHeight: .infinity, - alignment: .top) - .frameLimit(sizePortrait: 420) - - .padding(.top, 8) - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(ProfileLocalization.DeleteAccount.title) + }.padding(.horizontal, 24) + .frame(minHeight: 0, + maxHeight: .infinity, + alignment: .top) + .frameLimit(sizePortrait: 420) + + } // MARK: - Error Alert if viewModel.showError { @@ -135,7 +142,7 @@ public struct DeleteAccountView: View { } } .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift index f0a439d48..93ba44d04 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift @@ -30,105 +30,130 @@ public struct EditProfileView: View { public var body: some View { ZStack(alignment: .top) { - // MARK: - Page Body - ScrollView { - VStack { - Text(viewModel.profileChanges.profileType.localizedValue.capitalized) - .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textSecondary) - Button(action: { - withAnimation { - showingBottomSheet.toggle() + + // 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 { + viewModel.analytics.profileEditDoneClicked() + await viewModel.saveProfileUpdates() + } } - }, label: { - UserAvatar(url: viewModel.userModel.avatarUrl, image: $viewModel.inputImage) - .padding(.top, 30) - .overlay( - ZStack { - Circle().frame(width: 36, height: 36) - .foregroundColor(Theme.Colors.accentColor) - CoreAssets.addPhoto.swiftUIImage - .foregroundColor(.white) - }.offset(x: 36, y: 50) + }, + rightButtonIsActive: $viewModel.isChanged + ) + + // MARK: - Page Body + ScrollView { + VStack { + Text(viewModel.profileChanges.profileType.localizedValue.capitalized) + .font(Theme.Fonts.titleSmall) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) + Button(action: { + withAnimation { + showingBottomSheet.toggle() + } + }, label: { + UserAvatar(url: viewModel.userModel.avatarUrl, image: $viewModel.inputImage) + .padding(.top, 30) + .overlay( + ZStack { + Circle().frame(width: 36, height: 36) + .foregroundColor(CoreAssets.accentColor.swiftUIColor) + CoreAssets.addPhoto.swiftUIImage + .foregroundColor(.white) + }.offset(x: 36, y: 50) + ) + }).disabled(!viewModel.isEditable) + + Text(viewModel.userModel.name) + .font(Theme.Fonts.headlineSmall) + + Button(ProfileLocalization.switchTo + " " + + viewModel.profileChanges.profileType.switchToButtonTitle, + action: { + viewModel.switchProfile() + }).padding(.vertical, 24) + .font(Theme.Fonts.labelLarge) + + Group { + PickerView( + config: viewModel.yearsConfiguration, + router: viewModel.router ) - }).disabled(!viewModel.isEditable) - - Text(viewModel.userModel.name) - .font(Theme.Fonts.headlineSmall) - - Button(ProfileLocalization.switchTo + " " + - viewModel.profileChanges.profileType.switchToButtonTitle, - action: { - viewModel.switchProfile() - }).padding(.vertical, 24) - .font(Theme.Fonts.labelLarge) - - Group { - PickerView( - config: viewModel.yearsConfiguration, - router: viewModel.router - ) - if viewModel.isEditable { - VStack(alignment: .leading) { - PickerView(config: viewModel.countriesConfiguration, - router: viewModel.router) - - PickerView(config: viewModel.spokenLanguageConfiguration, - router: viewModel.router) - - Text(ProfileLocalization.Edit.Fields.aboutMe) - .font(Theme.Fonts.titleMedium) - TextEditor(text: $viewModel.profileChanges.shortBiography) - .padding(.horizontal, 12) - .padding(.vertical, 4) - .frame(height: 200) - .hideScrollContentBackground() - .background( - Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill( - Theme.Colors.textInputStroke - ) - ) + if viewModel.isEditable { + VStack(alignment: .leading) { + PickerView(config: viewModel.countriesConfiguration, + router: viewModel.router) + + PickerView(config: viewModel.spokenLanguageConfiguration, + router: viewModel.router) + + Text(ProfileLocalization.Edit.Fields.aboutMe) + .font(Theme.Fonts.titleMedium) + TextEditor(text: $viewModel.profileChanges.shortBiography) + .padding(.horizontal, 12) + .padding(.vertical, 4) + .frame(height: 200) + .hideScrollContentBackground() + .background( + Theme.Shapes.textInputShape + .fill(CoreAssets.textInputBackground.swiftUIColor) + ) + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill( + CoreAssets.textInputStroke.swiftUIColor + ) + ) + } } } - } - .onReceive(viewModel.yearsConfiguration.$text - .combineLatest(viewModel.countriesConfiguration.$text, - viewModel.spokenLanguageConfiguration.$text), - perform: { _ in - viewModel.checkChanges() - viewModel.checkProfileType() - }) - .onChange(of: viewModel.profileChanges) { _ in - viewModel.checkChanges() - viewModel.checkProfileType() - } - .onChange(of: viewModel.profileChanges.shortBiography, perform: { bio in - if bio.count > 300 { - viewModel.profileChanges.shortBiography.removeLast() + .onReceive(viewModel.yearsConfiguration.$text + .combineLatest(viewModel.countriesConfiguration.$text, + viewModel.spokenLanguageConfiguration.$text), + perform: { _ in + viewModel.checkChanges() + viewModel.checkProfileType() + }) + .onChange(of: viewModel.profileChanges) { _ in + viewModel.checkChanges() + viewModel.checkProfileType() } - }) - - Button(ProfileLocalization.Edit.deleteAccount, action: { - viewModel.trackProfileDeleteAccountClicked() - viewModel.router.showDeleteProfileView() - }) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.alert) - .padding(.top, 44) - - Spacer(minLength: 84) - }.padding(.horizontal, 24) - .sheet(isPresented: $showingImagePicker) { - ImagePickerView(image: $viewModel.inputImage) - .ignoresSafeArea() - } - }.padding(.top, 8) + .onChange(of: viewModel.profileChanges.shortBiography, perform: { bio in + if bio.count > 300 { + viewModel.profileChanges.shortBiography.removeLast() + } + }) + + Button(ProfileLocalization.Edit.deleteAccount, action: { + viewModel.analytics.profileDeleteAccountClicked() + viewModel.router.showDeleteProfileView() + }) + .font(Theme.Fonts.labelLarge) + .foregroundColor(CoreAssets.alert.swiftUIColor) + .padding(.top, 44) + + Spacer(minLength: 84) + }.padding(.horizontal, 24) + .sheet(isPresented: $showingImagePicker) { + ImagePickerView(image: $viewModel.inputImage) + .ignoresSafeArea() + } + } .onChange(of: showingImagePicker, perform: { value in if !value { if let image = viewModel.inputImage { @@ -145,9 +170,10 @@ public struct EditProfileView: View { self.profileDidEdit((viewModel.editedProfile, oldAvatar)) } } + .scrollAvoidKeyboard(dismissKeyboardByTap: true) .frameLimit(sizePortrait: 420) - .ignoresSafeArea(edges: .bottom) + }.ignoresSafeArea(edges: .bottom) // MARK: - Error Alert if viewModel.showError { VStack { @@ -168,7 +194,7 @@ public struct EditProfileView: View { HStack(alignment: .top, spacing: 6) { CoreAssets.alarm.swiftUIImage.renderingMode(.template) Text(viewModel.alertMessage ?? "") - }.shadowCardStyle(bgColor: Theme.Colors.warning, + }.shadowCardStyle(bgColor: CoreAssets.warning.swiftUIColor, textColor: .black) .transition(.move(edge: .bottom)) .onAppear { @@ -198,65 +224,24 @@ public struct EditProfileView: View { .padding(.horizontal) } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(true) - .navigationTitle(ProfileLocalization.editProfile) - .toolbar { - ToolbarItem(placement: .navigationBarLeading, content: { - Button(action: { - viewModel.backButtonTapped() - }, label: { - CoreAssets.arrowLeft.swiftUIImage - .renderingMode(.template) - .foregroundColor(Theme.Colors.accentColor) - }).opacity(viewModel.isChanged ? 1 : 0.3) - }) - ToolbarItem(placement: .navigationBarTrailing, content: { - Button(action: { - if viewModel.isChanged { - Task { - viewModel.trackProfileEditDoneClicked() - await viewModel.saveProfileUpdates() - } - } - }, label: { - HStack(spacing: 2) { - CoreAssets.done.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.accentColor) - Text(CoreLocalization.done) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.accentColor) - } - }).opacity(viewModel.isChanged ? 1 : 0.3) - }) - } .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) - .onDisappear { - if viewModel.profileChanges.isAvatarSaved { - self.profileDidEdit((viewModel.editedProfile, viewModel.inputImage)) - } else { - self.profileDidEdit((viewModel.editedProfile, oldAvatar)) - } - } } } #if DEBUG struct EditProfileView_Previews: PreviewProvider { static var previews: some View { - let userModel = UserProfile( - avatarUrl: "", - name: "Peter Parket", - username: "Peter", - dateJoined: Date(), - yearOfBirth: 0, - country: "Ukraine", - shortBiography: "", - isFullProfile: true - ) + let userModel = UserProfile(avatarUrl: "", + name: "Peter Parket", + username: "Peter", + dateJoined: Date(), + yearOfBirth: 0, + country: "Ukraine", + shortBiography: "", + isFullProfile: true) EditProfileView( viewModel: EditProfileViewModel( diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift index daa72ab39..ab76a9d76 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift @@ -75,17 +75,14 @@ public class EditProfileViewModel: ObservableObject { } } - let router: ProfileRouter - private let interactor: ProfileInteractorProtocol - private let analytics: ProfileAnalytics + let router: ProfileRouter + let analytics: ProfileAnalytics - public init( - userModel: UserProfile, - interactor: ProfileInteractorProtocol, - router: ProfileRouter, - analytics: ProfileAnalytics - ) { + public init(userModel: UserProfile, + interactor: ProfileInteractorProtocol, + router: ProfileRouter, + analytics: ProfileAnalytics) { self.userModel = userModel self.interactor = interactor self.router = router @@ -139,12 +136,12 @@ public class EditProfileViewModel: ObservableObject { withAnimation(.easeIn(duration: 0.1)) { self.isChanged = [spokenLanguageConfiguration.text.isEmpty ? false : spokenLanguageConfiguration.text != userModel.spokenLanguage, - yearsConfiguration.text.isEmpty ? false : yearsConfiguration.text != String(userModel.yearOfBirth), - countriesConfiguration.text.isEmpty ? false : countriesConfiguration.text != userModel.country, - userModel.shortBiography != profileChanges.shortBiography, - profileChanges.isAvatarChanged, - profileChanges.isAvatarDeleted, - userModel.isFullProfile != profileChanges.profileType.boolValue].contains(where: { $0 == true }) + yearsConfiguration.text.isEmpty ? false : yearsConfiguration.text != String(userModel.yearOfBirth), + countriesConfiguration.text.isEmpty ? false : countriesConfiguration.text != userModel.country, + userModel.shortBiography != profileChanges.shortBiography, + profileChanges.isAvatarChanged, + profileChanges.isAvatarDeleted, + userModel.isFullProfile != profileChanges.profileType.boolValue].contains(where: { $0 == true }) } } @@ -344,12 +341,4 @@ public class EditProfileViewModel: ObservableObject { profileChanges.shortBiography = userModel.shortBiography } - - func trackProfileDeleteAccountClicked() { - analytics.profileDeleteAccountClicked() - } - - func trackProfileEditDoneClicked() { - analytics.profileEditDoneClicked() - } } diff --git a/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift b/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift index 9a3f09330..79f100c2c 100644 --- a/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift +++ b/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift @@ -64,7 +64,7 @@ struct ProfileBottomSheet: View { VStack(alignment: .center, spacing: 4) { HStack(alignment: .center) { RoundedRectangle(cornerRadius: 2, style: .circular) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) .frame(width: 31, height: 4) .padding(.top, 4) }.frame(maxWidth: .infinity) @@ -97,7 +97,7 @@ struct ProfileBottomSheet: View { }.padding(.horizontal, 24) }.frame(maxWidth: idiom == .pad ? 330 : .infinity, maxHeight: 290, alignment: .topLeading) - .background(Theme.Colors.cardViewBackground) + .background(CoreAssets.cardViewBackground.swiftUIColor) .cornerRadius(8) .padding(.horizontal, 22) } @@ -159,7 +159,7 @@ extension ProfileBottomSheet { func bgColor() -> Color { switch self { case .gallery: - return Theme.Colors.accentColor + return CoreAssets.accentColor.swiftUIColor case .remove: return .clear case .cancel: @@ -170,11 +170,11 @@ extension ProfileBottomSheet { func frameColor() -> Color { switch self { case .gallery: - return Theme.Colors.accentColor + return CoreAssets.accentColor.swiftUIColor case .remove: - return Theme.Colors.alert + return CoreAssets.alert.swiftUIColor case .cancel: - return Theme.Colors.textInputStroke + return CoreAssets.textInputStroke.swiftUIColor } } @@ -183,9 +183,9 @@ extension ProfileBottomSheet { case .gallery: return .white case .remove: - return Theme.Colors.alert + return CoreAssets.alert.swiftUIColor case .cancel: - return Theme.Colors.textPrimary + return CoreAssets.textPrimary.swiftUIColor } } } diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index 2d61ed18c..ec3879bc5 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -11,192 +11,25 @@ import Kingfisher public struct ProfileView: View { - @StateObject private var viewModel: ProfileViewModel - @Binding var settingsTapped: Bool + @ObservedObject private var viewModel: ProfileViewModel - public init(viewModel: ProfileViewModel, settingsTapped: Binding) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) - self._settingsTapped = settingsTapped + public init(viewModel: ProfileViewModel) { + self.viewModel = viewModel + Task { + await viewModel.getMyProfile() + } } public var body: some View { ZStack(alignment: .top) { - // MARK: - Page Body - RefreshableScrollViewCompat(action: { - await viewModel.getMyProfile(withProgress: false) - }) { - VStack { - if viewModel.isShowProgress { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 200) - .padding(.horizontal) - } else { - UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar) - .padding(.top, 30) - Text(viewModel.userModel?.name ?? "") - .font(Theme.Fonts.headlineSmall) - .padding(.top, 20) - - Text("@\(viewModel.userModel?.username ?? "")") - .font(Theme.Fonts.labelLarge) - .padding(.top, 4) - .foregroundColor(Theme.Colors.textSecondary) - .padding(.bottom, 10) - - // MARK: - Profile Info - if viewModel.userModel?.yearOfBirth != 0 || viewModel.userModel?.shortBiography != "" { - VStack(alignment: .leading, spacing: 14) { - Text(ProfileLocalization.info) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - - VStack(alignment: .leading, spacing: 16) { - if viewModel.userModel?.yearOfBirth != 0 { - HStack { - Text(ProfileLocalization.Edit.Fields.yearOfBirth) - .foregroundColor(Theme.Colors.textSecondary) - Text(String(viewModel.userModel?.yearOfBirth ?? 0)) - } - } - if let bio = viewModel.userModel?.shortBiography, bio != "" { - HStack(alignment: .top) { - Text(ProfileLocalization.bio + " ") - .foregroundColor(Theme.Colors.textSecondary) - + Text(bio) - } - } - } - .cardStyle( - bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear - ) - }.padding(.bottom, 16) - } - - VStack(alignment: .leading, spacing: 14) { - // MARK: - Settings - Text(ProfileLocalization.settings) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - VStack(alignment: .leading, spacing: 27) { - Button(action: { - viewModel.trackProfileVideoSettingsClicked() - viewModel.router.showSettings() - }, label: { - HStack { - Text(ProfileLocalization.settingsVideo) - Spacer() - Image(systemName: "chevron.right") - } - }) - - }.cardStyle( - bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear - ) - - // MARK: - Support info - Text(ProfileLocalization.supportInfo) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - VStack(alignment: .leading, spacing: 24) { - if let support = viewModel.contactSupport() { - Button(action: { - viewModel.trackEmailSupportClicked() - UIApplication.shared.open(support) - }, label: { - HStack { - Text(ProfileLocalization.contact) - Spacer() - Image(systemName: "chevron.right") - } - }) - .buttonStyle(PlainButtonStyle()) - .foregroundColor(.primary) - Rectangle() - .frame(height: 1) - .foregroundColor(Theme.Colors.textSecondary) - } - - if let tos = viewModel.config.termsOfUse { - Button(action: { - viewModel.trackCookiePolicyClicked() - UIApplication.shared.open(tos) - }, label: { - HStack { - Text(ProfileLocalization.terms) - Spacer() - Image(systemName: "chevron.right") - } - }) - .buttonStyle(PlainButtonStyle()) - .foregroundColor(.primary) - Rectangle() - .frame(height: 1) - .foregroundColor(Theme.Colors.textSecondary) - } - - if let privacy = viewModel.config.privacyPolicy { - Button(action: { - viewModel.trackPrivacyPolicyClicked() - UIApplication.shared.open(privacy) - }, label: { - HStack { - Text(ProfileLocalization.privacy) - Spacer() - Image(systemName: "chevron.right") - } - }) - .buttonStyle(PlainButtonStyle()) - .foregroundColor(.primary) - } - }.cardStyle( - bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear - ) - - // MARK: - Log out - VStack { - Button(action: { - viewModel.router.presentView(transitionStyle: .crossDissolve) { - AlertView( - alertTitle: ProfileLocalization.LogoutAlert.title, - alertMessage: ProfileLocalization.LogoutAlert.text, - positiveAction: CoreLocalization.Alert.accept, - onCloseTapped: { - viewModel.router.dismiss(animated: true) - }, - okTapped: { - viewModel.router.dismiss(animated: true) - Task { - await viewModel.logOut() - } - }, type: .logOut - ) - } - }, label: { - HStack { - Text(ProfileLocalization.logout) - Spacer() - Image(systemName: "rectangle.portrait.and.arrow.right") - } - }) - - } - .foregroundColor(Theme.Colors.alert) - .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear) - .padding(.top, 24) - .padding(.bottom, 60) - } - Spacer() - } - } - }.frameLimit(sizePortrait: 420) - .padding(.top, 8) - .onChange(of: settingsTapped, perform: { _ in + + // MARK: - Page name + VStack(alignment: .center) { + NavigationBar(title: ProfileLocalization.title, + rightButtonType: .edit, + rightButtonAction: { if let userModel = viewModel.userModel { - viewModel.trackProfileEditClicked() + viewModel.analytics.profileEditClicked() viewModel.router.showEditProfile( userModel: userModel, avatar: viewModel.updatedAvatar, @@ -210,14 +43,186 @@ public struct ProfileView: View { } ) } - }) - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) + }, rightButtonIsActive: .constant(viewModel.connectivity.isInternetAvaliable)) + + // MARK: - Page Body + + RefreshableScrollViewCompat(action: { + await viewModel.getMyProfile(withProgress: isIOS14) + }) { + VStack { + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } else { + UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar) + .padding(.top, 30) + Text(viewModel.userModel?.name ?? "") + .font(Theme.Fonts.headlineSmall) + .padding(.top, 20) + + Text("@\(viewModel.userModel?.username ?? "")") + .font(Theme.Fonts.labelLarge) + .padding(.top, 4) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) + .padding(.bottom, 10) + + // MARK: - Profile Info + if viewModel.userModel?.yearOfBirth != 0 || viewModel.userModel?.shortBiography != "" { + VStack(alignment: .leading, spacing: 14) { + Text(ProfileLocalization.info) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + + VStack(alignment: .leading, spacing: 16) { + if viewModel.userModel?.yearOfBirth != 0 { + HStack { + Text(ProfileLocalization.Edit.Fields.yearOfBirth) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) + Text(String(viewModel.userModel?.yearOfBirth ?? 0)) + } + } + if let bio = viewModel.userModel?.shortBiography, bio != "" { + HStack(alignment: .top) { + Text(ProfileLocalization.bio + " ") + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) + + Text(bio) + } + } + } + .cardStyle( + bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor, + strokeColor: .clear + ) + }.padding(.bottom, 16) + } + + VStack(alignment: .leading, spacing: 14) { + // MARK: - Settings + Text(ProfileLocalization.settings) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + VStack(alignment: .leading, spacing: 27) { + HStack { + Button(action: { + viewModel.analytics.profileVideoSettingsClicked() + viewModel.router.showSettings() + }, label: { + Text(ProfileLocalization.settingsVideo) + Spacer() + Image(systemName: "chevron.right") + }) + } + }.cardStyle( + bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor, + strokeColor: .clear + ) + + // MARK: - Support info + Text(ProfileLocalization.supportInfo) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + VStack(alignment: .leading, spacing: 24) { + if let support = viewModel.contactSupport() { + 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 { + 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 { + 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, + strokeColor: .clear + ) + + // MARK: - Log out + VStack { + HStack { + Button(action: { + viewModel.router.presentView(transitionStyle: .crossDissolve) { + AlertView( + alertTitle: ProfileLocalization.LogoutAlert.title, + alertMessage: ProfileLocalization.LogoutAlert.text, + positiveAction: CoreLocalization.Alert.accept, + onCloseTapped: { + viewModel.router.dismiss(animated: true) + }, + okTapped: { + Task { + viewModel.analytics.userLogout(force: false) + await viewModel.logOut() + } + viewModel.router.dismiss(animated: true) + }, type: .logOut + ) + } + }, label: { + Text(ProfileLocalization.logout) + Spacer() + Image(systemName: "rectangle.portrait.and.arrow.right") + }) + } + }.foregroundColor(CoreAssets.alert.swiftUIColor) + .cardStyle(bgColor: CoreAssets.textInputUnfocusedBackground.swiftUIColor, + strokeColor: .clear) + .padding(.top, 24) + .padding(.bottom, 60) + } + Spacer() + } + } + }.frameLimit(sizePortrait: 420) + + } // MARK: - Offline mode SnackBar OfflineSnackBarView(connectivity: viewModel.connectivity, reloadAction: { - await viewModel.getMyProfile(withProgress: false) + await viewModel.getMyProfile(withProgress: isIOS14) }) // MARK: - Error Alert @@ -236,13 +241,8 @@ public struct ProfileView: View { } } } - .onFirstAppear { - Task { - await viewModel.getMyProfile() - } - } .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } @@ -258,12 +258,12 @@ struct ProfileView_Previews: PreviewProvider { config: ConfigMock(), connectivity: Connectivity()) - ProfileView(viewModel: vm, settingsTapped: .constant(false)) + ProfileView(viewModel: vm) .preferredColorScheme(.light) .previewDisplayName("DiscoveryView Light") .loadFonts() - ProfileView(viewModel: vm, settingsTapped: .constant(false)) + ProfileView(viewModel: vm) .preferredColorScheme(.dark) .previewDisplayName("DiscoveryView Dark") .loadFonts() @@ -288,7 +288,7 @@ struct UserAvatar: View { var body: some View { ZStack { Circle() - .foregroundColor(Theme.Colors.avatarStroke) + .foregroundColor(CoreAssets.avatarStroke.swiftUIColor) .frame(width: 104, height: 104) if let image { Image(uiImage: image) diff --git a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift index 49e5dd254..8439adbc2 100644 --- a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift +++ b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift @@ -23,21 +23,17 @@ public class ProfileViewModel: ObservableObject { } } - + private let interactor: ProfileInteractorProtocol let router: ProfileRouter + let analytics: ProfileAnalytics let config: Config let connectivity: ConnectivityProtocol - private let interactor: ProfileInteractorProtocol - private let analytics: ProfileAnalytics - - public init( - interactor: ProfileInteractorProtocol, - router: ProfileRouter, - analytics: ProfileAnalytics, - config: Config, - connectivity: ConnectivityProtocol - ) { + public init(interactor: ProfileInteractorProtocol, + router: ProfileRouter, + analytics: ProfileAnalytics, + config: Config, + connectivity: ConnectivityProtocol) { self.interactor = interactor self.router = router self.analytics = analytics @@ -83,9 +79,8 @@ public class ProfileViewModel: ObservableObject { @MainActor func logOut() async { do { - try await interactor.logOut() - router.showLoginScreen() - analytics.userLogout(force: false) + try await self.interactor.logOut() + self.router.showLoginScreen() } catch let error { if error.isInternetError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection @@ -94,24 +89,4 @@ public class ProfileViewModel: ObservableObject { } } } - - func trackProfileVideoSettingsClicked() { - analytics.profileVideoSettingsClicked() - } - - func trackEmailSupportClicked() { - analytics.emailSupportClicked() - } - - func trackCookiePolicyClicked() { - analytics.cookiePolicyClicked() - } - - func trackPrivacyPolicyClicked() { - analytics.privacyPolicyClicked() - } - - func trackProfileEditClicked() { - analytics.profileEditClicked() - } } diff --git a/Profile/Profile/Presentation/Settings/SettingsView.swift b/Profile/Profile/Presentation/Settings/SettingsView.swift index 1e940716f..84b6869af 100644 --- a/Profile/Profile/Presentation/Settings/SettingsView.swift +++ b/Profile/Profile/Presentation/Settings/SettingsView.swift @@ -21,47 +21,53 @@ public struct SettingsView: View { public var body: some View { ZStack(alignment: .top) { - // MARK: - Page Body - ScrollView { - VStack(alignment: .leading, spacing: 24) { - if viewModel.isShowProgress { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 200) - .padding(.horizontal) - } else { - // MARK: Wi-fi - HStack { - SettingsCell( - title: ProfileLocalization.Settings.wifiTitle, - description: ProfileLocalization.Settings.wifiDescription - ) - Toggle(isOn: $viewModel.wifiOnly, label: {}) - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .frame(width: 50) - }.foregroundColor(Theme.Colors.textPrimary) - Divider() - - // MARK: Download Quality - HStack { - Button(action: { - viewModel.router.showVideoQualityView(viewModel: viewModel) - }, label: { - SettingsCell(title: ProfileLocalization.Settings.videoQualityTitle, - description: viewModel.selectedQuality.settingsDescription()) - }) - // Spacer() - Image(systemName: "chevron.right") - .padding(.trailing, 12) - .frame(width: 10) + // MARK: - Page name + VStack(alignment: .center) { + NavigationBar(title: ProfileLocalization.Settings.videoSettingsTitle, + leftButtonAction: { viewModel.router.back() }) + + // MARK: - Page Body + + ScrollView { + VStack(alignment: .leading, spacing: 24) { + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } else { + // MARK: Wi-fi + HStack { + SettingsCell( + title: ProfileLocalization.Settings.wifiTitle, + description: ProfileLocalization.Settings.wifiDescription + ) + Toggle(isOn: $viewModel.wifiOnly, label: {}) + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .frame(width: 50) + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) + Divider() + + // MARK: Download Quality + HStack { + Button(action: { + viewModel.router.showVideoQualityView(viewModel: viewModel) + }, label: { + SettingsCell(title: ProfileLocalization.Settings.videoQualityTitle, + description: viewModel.selectedQuality.settingsDescription()) + }) + // Spacer() + Image(systemName: "chevron.right") + .padding(.trailing, 12) + .frame(width: 10) + } + Divider() } - Divider() - } - }.frame(minWidth: 0, - maxWidth: .infinity, - alignment: .topLeading) - .padding(.horizontal, 24) - }.frameLimit(sizePortrait: 420) - .padding(.top, 8) + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + .padding(.horizontal, 24) + }.frameLimit(sizePortrait: 420) + } // MARK: - Error Alert if viewModel.showError { @@ -77,11 +83,8 @@ public struct SettingsView: View { } } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(ProfileLocalization.Settings.videoSettingsTitle) .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } @@ -126,9 +129,9 @@ public struct SettingsCell: View { if let description { Text(description) .font(Theme.Fonts.labelMedium) - .foregroundColor(Theme.Colors.textSecondary) + .foregroundColor(CoreAssets.textSecondary.swiftUIColor) } - }.foregroundColor(Theme.Colors.textPrimary) + }.foregroundColor(CoreAssets.textPrimary.swiftUIColor) .frame(maxWidth: .infinity, alignment: .leading) } } diff --git a/Profile/Profile/Presentation/Settings/VideoQualityView.swift b/Profile/Profile/Presentation/Settings/VideoQualityView.swift index a82c67747..bcf071d6a 100644 --- a/Profile/Profile/Presentation/Settings/VideoQualityView.swift +++ b/Profile/Profile/Presentation/Settings/VideoQualityView.swift @@ -20,41 +20,50 @@ public struct VideoQualityView: View { public var body: some View { ZStack(alignment: .top) { - // MARK: - Page Body - ScrollView { - VStack(alignment: .leading, spacing: 24) { - if viewModel.isShowProgress { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 200) - .padding(.horizontal) - } else { - - ForEach(viewModel.quality, id: \.offset) { _, quality in - Button(action: { - viewModel.selectedQuality = quality - }, label: { - HStack { - SettingsCell( - title: quality.title(), - description: quality.description() - ) - Spacer() - CoreAssets.checkmark.swiftUIImage - .renderingMode(.template) - .foregroundColor(.accentColor) - .opacity(quality == viewModel.selectedQuality ? 1 : 0) - - }.foregroundColor(Theme.Colors.textPrimary) - }) - Divider() + + // MARK: - Page name + VStack(alignment: .center) { + NavigationBar(title: ProfileLocalization.Settings.videoQualityTitle, + leftButtonAction: { viewModel.router.back() }) + + // MARK: - Page Body + + ScrollView { + VStack(alignment: .leading, spacing: 24) { + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } else { + + ForEach(viewModel.quality, id: \.offset) { _, quality in + Button(action: { + viewModel.selectedQuality = quality + }, label: { + 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) + }) + Divider() + } + } - } - }.frame(minWidth: 0, - maxWidth: .infinity, - alignment: .topLeading) - .padding(.horizontal, 24) - }.frameLimit(sizePortrait: 420) - .padding(.top, 8) + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + .padding(.horizontal, 24) + }.frameLimit(sizePortrait: 420) + + } // MARK: - Error Alert if viewModel.showError { @@ -70,11 +79,8 @@ public struct VideoQualityView: View { } } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(ProfileLocalization.Settings.videoQualityTitle) .background( - Theme.Colors.background + CoreAssets.background.swiftUIColor .ignoresSafeArea() ) } diff --git a/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift index 2980fb4a8..47280627c 100644 --- a/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift @@ -761,66 +761,4 @@ final class EditProfileViewModelTests: XCTestCase { Verify(interactor, 1, .getSpokenLanguages()) Verify(interactor, 1, .getCountries()) } - - func testTrackProfileDeleteAccountClicked() { - let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let userModel = UserProfile( - avatarUrl: "url", - name: "Test", - username: "Name", - dateJoined: Date(), - yearOfBirth: 1986, - country: "UA", - spokenLanguage: "UA", - shortBiography: "Bio", - isFullProfile: false - ) - - Given(interactor, .getSpokenLanguages(willReturn: [])) - Given(interactor, .getCountries(willReturn: [])) - - let viewModel = EditProfileViewModel( - userModel: userModel, - interactor: interactor, - router: router, - analytics: analytics - ) - - viewModel.trackProfileDeleteAccountClicked() - - Verify(analytics, 1, .profileDeleteAccountClicked()) - } - - func testTrackProfileEditDoneClicked() { - let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let userModel = UserProfile( - avatarUrl: "url", - name: "Test", - username: "Name", - dateJoined: Date(), - yearOfBirth: 1986, - country: "UA", - spokenLanguage: "UA", - shortBiography: "Bio", - isFullProfile: false - ) - - Given(interactor, .getSpokenLanguages(willReturn: [])) - Given(interactor, .getCountries(willReturn: [])) - - let viewModel = EditProfileViewModel( - userModel: userModel, - interactor: interactor, - router: router, - analytics: analytics - ) - - viewModel.trackProfileEditDoneClicked() - - Verify(analytics, 1, .profileEditDoneClicked()) - } } diff --git a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift index ddc0f356f..6c6502921 100644 --- a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift @@ -19,24 +19,21 @@ final class ProfileViewModelTests: XCTestCase { let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) - - let user = UserProfile( - avatarUrl: "", - name: "Steve", - username: "Steve", - dateJoined: Date(), - yearOfBirth: 2000, - country: "Ua", - shortBiography: "Bio", - isFullProfile: false - ) + + let viewModel = ProfileViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + connectivity: connectivity) + + let user = UserProfile(avatarUrl: "", + name: "Steve", + username: "Steve", + dateJoined: Date(), + yearOfBirth: 2000, + country: "Ua", + shortBiography: "Bio", + isFullProfile: false) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .getMyProfile(willReturn: user)) @@ -56,24 +53,21 @@ final class ProfileViewModelTests: XCTestCase { let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) - - let user = UserProfile( - avatarUrl: "", - name: "Steve", - username: "Steve", - dateJoined: Date(), - yearOfBirth: 2000, - country: "Ua", - shortBiography: "Bio", - isFullProfile: false - ) + + let viewModel = ProfileViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + connectivity: connectivity) + + let user = UserProfile(avatarUrl: "", + name: "Steve", + username: "Steve", + dateJoined: Date(), + yearOfBirth: 2000, + country: "Ua", + shortBiography: "Bio", + isFullProfile: false) Given(connectivity, .isInternetAvaliable(getter: false)) Given(interactor, .getMyProfileOffline(willReturn: user)) @@ -93,13 +87,12 @@ final class ProfileViewModelTests: XCTestCase { let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) + + let viewModel = ProfileViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + connectivity: connectivity) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -120,13 +113,12 @@ final class ProfileViewModelTests: XCTestCase { let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) + + let viewModel = ProfileViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + connectivity: connectivity) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .getMyProfile(willThrow: NoCachedDataError())) @@ -145,13 +137,12 @@ final class ProfileViewModelTests: XCTestCase { let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) + + let viewModel = ProfileViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + connectivity: connectivity) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .getMyProfile(willThrow: NSError())) @@ -170,13 +161,12 @@ final class ProfileViewModelTests: XCTestCase { let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) + + let viewModel = ProfileViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + connectivity: connectivity) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .logOut(willProduce: {_ in})) @@ -192,13 +182,12 @@ final class ProfileViewModelTests: XCTestCase { let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) + + let viewModel = ProfileViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + connectivity: connectivity) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -216,13 +205,12 @@ final class ProfileViewModelTests: XCTestCase { let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) + + let viewModel = ProfileViewModel(interactor: interactor, + router: router, + analytics: analytics, + config: ConfigMock(), + connectivity: connectivity) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .logOut(willThrow: NSError())) @@ -232,95 +220,5 @@ final class ProfileViewModelTests: XCTestCase { XCTAssertTrue(viewModel.showError) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) } - - func testTrackProfileVideoSettingsClicked() { - 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 - ) - - viewModel.trackProfileVideoSettingsClicked() - - Verify(analytics, 1, .profileVideoSettingsClicked()) - } - - func testTrackEmailSupportClicked() { - 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 - ) - - viewModel.trackEmailSupportClicked() - - Verify(analytics, 1, .emailSupportClicked()) - } - - func testTrackCookiePolicyClicked() { - 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 - ) - - viewModel.trackCookiePolicyClicked() - - Verify(analytics, 1, .cookiePolicyClicked()) - } - - func testTrackPrivacyPolicyClicked() { - 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 - ) - - viewModel.trackPrivacyPolicyClicked() - - Verify(analytics, 1, .privacyPolicyClicked()) - } - - func testTrackProfileEditClicked() { - 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 - ) - - viewModel.trackProfileEditClicked() - - Verify(analytics, 1, .profileEditClicked()) - } } diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index 8ebac614f..5c111a812 100644 --- a/Profile/ProfileTests/ProfileMock.generated.swift +++ b/Profile/ProfileTests/ProfileMock.generated.swift @@ -1125,12 +1125,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { perform?(`blocks`) } - open func deleteAllFiles() { - addInvocation(.m_deleteAllFiles) - let perform = methodPerformValue(.m_deleteAllFiles) as? () -> Void - perform?() - } - open func fileUrl(for blockId: String) -> URL? { addInvocation(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) let perform = methodPerformValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void @@ -1153,7 +1147,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_resumeDownloading case m_pauseDownloading case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) - case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -1185,8 +1178,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_deleteAllFiles, .m_deleteAllFiles): return .match - case (.m_fileUrl__for_blockId(let lhsBlockid), .m_fileUrl__for_blockId(let rhsBlockid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) @@ -1204,7 +1195,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return 0 case .m_pauseDownloading: return 0 case let .m_deleteFile__blocks_blocks(p0): return p0.intValue - case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue } } @@ -1217,7 +1207,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_resumeDownloading: return ".resumeDownloading()" case .m_pauseDownloading: return ".pauseDownloading()" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" - case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" } } @@ -1304,7 +1293,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} - public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} } @@ -1333,9 +1321,6 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) } - public static func deleteAllFiles(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_deleteAllFiles, performs: perform) - } public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } diff --git a/README.md b/README.md index 668a7a554..2495c8fae 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,23 @@ This project uses custom APIs to improve performance and reduce the number of re You can find the plugin with the API and installation guide [here](https://github.com/raccoongang/mobile-api-extensions). +## Roadmap +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 +- Prerequisite course +- Prerequisite sections +- Scorm XBlocks +- Native Programs +- New discovery (catalog) +- E-Commerce + ## License -The code in this repository is licensed under the Apache-2.0 license unless otherwise noted. +The code in this repository is licensed under the AGPL v3 license unless otherwise noted. Please see [LICENSE](https://github.com/raccoongang/educationx-app-ios/blob/main/LICENSE) file for details. diff --git a/docs/0001-strategy-for-maintaining-OS-versions.rst b/docs/0001-strategy-for-maintaining-OS-versions.rst deleted file mode 100644 index 17aea4b7d..000000000 --- a/docs/0001-strategy-for-maintaining-OS-versions.rst +++ /dev/null @@ -1,62 +0,0 @@ -Title: Strategy for maintaining iOS versions in the OpenEdx Project -================================================== -Date: 13 September 2023 - -Status ------- -Accepted - -Context ------- -In the OpenEdx project, we are developing a mobile application on the SwiftUI platform for iOS users. -To ensure optimal support and security of the application, we need to make a decision regarding which -versions of the iOS operating system will be supported. This document outlines the decision to support -only the current iOS version and the two previous versions. - -Decision ------- -We decide to support only the current iOS version and the two previous versions. This means that our -application will be optimized and tested to work on the three most recent iOS versions at the time -of the application's release. - -Why is this important? - -1. Streamlined Development and Testing ------- -Supporting multiple iOS versions requires significant development and testing resources. By restricting -the number of supported versions, we can focus our efforts on developing new features and improving the -application's quality, without spreading ourselves too thin trying to maintain compatibility -with outdated iOS versions. - -2. Performance and User Experience Enhancement ------- -With each new iOS version, Apple introduces performance and functionality improvements. By limiting support -to older iOS versions, we can leverage new capabilities and libraries to create faster and more feature-rich -application versions. This also enhances the user experience and user satisfaction. - -3. Security ------- -The most crucial aspect of mobile application development is ensuring user security. New iOS versions -contain critical security updates, and supporting old iOS versions can leave the application vulnerable -to known threats. By supporting only the current version and the two previous ones, we can quickly respond -to security updates and provide robust data protection for users. - -Project Impact ------- - -This decision will impact the project in the following ways: ------- -Enhanced application security. -Improved performance and functionality. -Reduced development and testing burden. -Implementation - -To implement this decision, we will monitor the releases of new iOS versions and update our application -accordingly, considering the limitation of supporting only the current version and the two previous versions. -We will also inform users about the need to update their operating systems for optimal application performance. - -Alternatives ------- -Continuing to support older iOS versions would demand more resources, pose security and performance risks, -and limit our ability to adopt modern technologies and innovations, potentially slowing down development -and compromising user experience.