From b73c0c305fdb091ea0cf4456d6246ca12c756800 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 30 Jul 2025 08:19:11 +0300 Subject: [PATCH 01/51] fix: resolve issue 581 --- Core/Core.xcodeproj/project.pbxproj | 4 --- .../Container/CourseContainerViewModel.swift | 33 +++++++++++++++++-- .../Outline/CourseOutlineView.swift | 2 ++ Podfile.lock | 2 +- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 7c3cd8479..c60200710 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -1080,14 +1080,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests-resources.sh\"\n"; diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index b6da7b531..a8162b51d 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -193,11 +193,40 @@ public final class CourseContainerViewModel: BaseCourseViewModel { sequentialIndex: continueWith.sequentialIndex ) } - + + private func getCourseBlocksWithTimeout( + courseID: String, + timeoutSeconds: UInt64 + ) async throws -> CourseStructure? { + return try await withThrowingTaskGroup(of: CourseStructure?.self) { group in + + group.addTask { + try await self.interactor.getCourseBlocks(courseID: courseID) + } + + group.addTask { + try await Task.sleep(nanoseconds: timeoutSeconds * 1_000_000_000) + return nil + } + + guard let firstResult = try await group.next() else { + return nil + } + + group.cancelAll() + + return firstResult + } + } + @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - return try await interactor.getCourseBlocks(courseID: courseID) + if let test = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: 15) { + return test + } + + return try await interactor.getLoadedCourseBlocks(courseID: courseID) } else { return try await interactor.getLoadedCourseBlocks(courseID: courseID) } diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 2c2c8565e..8872771a2 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -67,7 +67,9 @@ public struct CourseOutlineView: View { collapsed: $collapsed, viewHeight: $viewHeight ) + RefreshProgressView(isShowRefresh: $viewModel.isShowRefresh) + VStack(alignment: .leading) { if isVideo, diff --git a/Podfile.lock b/Podfile.lock index ab2424669..92ba93fc7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -36,4 +36,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 2d71ad797d49fa32b47c3315b92159de82824103 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 From cc0e655031941766bb2d8fc1c7d6c65e70d1b0bd Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 30 Jul 2025 11:10:56 +0300 Subject: [PATCH 02/51] fix: update adjustments --- .../Presentation/Container/CourseContainerViewModel.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index a8162b51d..5cfd87ead 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -225,7 +225,9 @@ public final class CourseContainerViewModel: BaseCourseViewModel { if let test = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: 15) { return test } - + connectivity.internetReachableSubject.send(.notReachable) + isShowProgress = false + isShowRefresh = false return try await interactor.getLoadedCourseBlocks(courseID: courseID) } else { return try await interactor.getLoadedCourseBlocks(courseID: courseID) From 84a215601746183af43bbd82d6390409ac8b7b65 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 30 Jul 2025 11:22:35 +0300 Subject: [PATCH 03/51] fix: add timeout constant --- .../Presentation/Container/CourseContainerViewModel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 5cfd87ead..2496e1f52 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -109,6 +109,9 @@ public final class CourseContainerViewModel: BaseCourseViewModel { private let interactor: CourseInteractorProtocol private let authInteractor: AuthInteractorProtocol + + private let timeOutIntervalSeconds: UInt64 = 15 + let analytics: CourseAnalytics let coreAnalytics: CoreAnalytics private(set) var storage: CourseStorage @@ -222,7 +225,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel { @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - if let test = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: 15) { + if let test = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: timeOutIntervalSeconds) { return test } connectivity.internetReachableSubject.send(.notReachable) From cd349b3f22fcb06008f2991bc895f926575d89ab Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 30 Jul 2025 11:30:59 +0300 Subject: [PATCH 04/51] fix: adjust naming --- .../Presentation/Container/CourseContainerViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 2496e1f52..74e40265b 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -225,8 +225,8 @@ public final class CourseContainerViewModel: BaseCourseViewModel { @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - if let test = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: timeOutIntervalSeconds) { - return test + if let courseStructure = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: timeOutIntervalSeconds) { + return courseStructure } connectivity.internetReachableSubject.send(.notReachable) isShowProgress = false From 5aa8e55f0cde4c6c6580338402fd2156ef5de2f9 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 30 Jul 2025 16:42:23 +0300 Subject: [PATCH 05/51] fix: update connectivity --- Core/Core/Configuration/Connectivity.swift | 110 +++++++++++++----- .../Container/CourseContainerViewModel.swift | 8 +- 2 files changed, 80 insertions(+), 38 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 864e10e9f..741791b19 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -1,9 +1,9 @@ -// -// Connectivity.swift -// OpenEdX -// -// Created by  Stepanok Ivan on 15.12.2022. -// +//// +//// Connectivity.swift +//// OpenEdX +//// +//// Created by  Stepanok Ivan on 15.12.2022. +//// import Alamofire import Combine @@ -24,13 +24,24 @@ public protocol ConnectivityProtocol: Sendable { @MainActor public class Connectivity: ConnectivityProtocol { - let networkManager = NetworkReachabilityManager() - - public var isInternetAvaliable: Bool { - // false - networkManager?.isReachable ?? false - } - + + private let networkManager = NetworkReachabilityManager() + private let verificationURL: URL + private let verificationTimeout: TimeInterval + + private static var lastVerificationDate: TimeInterval? + private static var lastVerificationResult: Bool = false + + private var _isInternetAvailable: Bool = true { + didSet { + internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) + } + } + + public var isInternetAvaliable: Bool { + return _isInternetAvailable + } + public var isMobileData: Bool { if let networkManager { return networkManager.isReachableOnCellular @@ -38,31 +49,68 @@ public class Connectivity: ConnectivityProtocol { return false } } - + public let internetReachableSubject = CurrentValueSubject(nil) - - public init() { + + public init( + urlToVerify: URL = Config().baseURL, + timeout: TimeInterval = 15 + ) { + self.verificationURL = urlToVerify + self.verificationTimeout = timeout checkInternet() } - - func checkInternet() { - if let networkManager { - networkManager.startListening { status in - DispatchQueue.main.async { - switch status { - case .unknown: - self.internetReachableSubject.send(InternetState.notReachable) - case .notReachable: - self.internetReachableSubject.send(InternetState.notReachable) - case .reachable: - self.internetReachableSubject.send(InternetState.reachable) + + deinit { + networkManager?.stopListening() + } + + private func checkInternet() { + guard let nm = networkManager else { + _isInternetAvailable = false + return + } + + nm.startListening { [weak self] status in + DispatchQueue.main.async { + guard let self = self else { return } + switch status { + case .reachable: + let nowTS = Date().timeIntervalSince1970 + if let lastTS = Connectivity.lastVerificationDate, + nowTS - lastTS < 30 { + self._isInternetAvailable = Connectivity.lastVerificationResult + } else { + Task { @MainActor in + let live = await self.verifyInternet() + Connectivity.lastVerificationDate = Date().timeIntervalSince1970 + Connectivity.lastVerificationResult = live + self._isInternetAvailable = live + } } + case .notReachable, .unknown: + Connectivity.lastVerificationDate = nil + Connectivity.lastVerificationResult = false + self._isInternetAvailable = false } } - } else { - DispatchQueue.main.async { - self.internetReachableSubject.send(InternetState.notReachable) + } + } + + private func verifyInternet() async -> Bool { + var request = URLRequest(url: verificationURL) + request.httpMethod = "HEAD" + request.timeoutInterval = verificationTimeout + + do { + let (_, response) = try await URLSession.shared.data(for: request) + if let http = response as? HTTPURLResponse, + (200..<400).contains(http.statusCode) { + return true } + } catch { + return false } + return false } } diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 74e40265b..31950d661 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -225,13 +225,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel { @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - if let courseStructure = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: timeOutIntervalSeconds) { - return courseStructure - } - connectivity.internetReachableSubject.send(.notReachable) - isShowProgress = false - isShowRefresh = false - return try await interactor.getLoadedCourseBlocks(courseID: courseID) + return try await self.interactor.getCourseBlocks(courseID: courseID) } else { return try await interactor.getLoadedCourseBlocks(courseID: courseID) } From 64c59a149887023fa90968c551b6001fc62ac838 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 13:27:21 +0300 Subject: [PATCH 06/51] fix: remove unused functions and hardcoded variables --- Core/Core/Configuration/Connectivity.swift | 3 +- .../Container/CourseContainerViewModel.swift | 29 +------------------ 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 741791b19..0a1a58ff8 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -28,6 +28,7 @@ public class Connectivity: ConnectivityProtocol { private let networkManager = NetworkReachabilityManager() private let verificationURL: URL private let verificationTimeout: TimeInterval + private let secondsPast: TimeInterval = 30 private static var lastVerificationDate: TimeInterval? private static var lastVerificationResult: Bool = false @@ -78,7 +79,7 @@ public class Connectivity: ConnectivityProtocol { case .reachable: let nowTS = Date().timeIntervalSince1970 if let lastTS = Connectivity.lastVerificationDate, - nowTS - lastTS < 30 { + nowTS - lastTS < self.secondsPast { self._isInternetAvailable = Connectivity.lastVerificationResult } else { Task { @MainActor in diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 31950d661..c6aa68584 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -110,8 +110,6 @@ public final class CourseContainerViewModel: BaseCourseViewModel { private let interactor: CourseInteractorProtocol private let authInteractor: AuthInteractorProtocol - private let timeOutIntervalSeconds: UInt64 = 15 - let analytics: CourseAnalytics let coreAnalytics: CoreAnalytics private(set) var storage: CourseStorage @@ -197,35 +195,10 @@ public final class CourseContainerViewModel: BaseCourseViewModel { ) } - private func getCourseBlocksWithTimeout( - courseID: String, - timeoutSeconds: UInt64 - ) async throws -> CourseStructure? { - return try await withThrowingTaskGroup(of: CourseStructure?.self) { group in - - group.addTask { - try await self.interactor.getCourseBlocks(courseID: courseID) - } - - group.addTask { - try await Task.sleep(nanoseconds: timeoutSeconds * 1_000_000_000) - return nil - } - - guard let firstResult = try await group.next() else { - return nil - } - - group.cancelAll() - - return firstResult - } - } - @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - return try await self.interactor.getCourseBlocks(courseID: courseID) + return try await interactor.getCourseBlocks(courseID: courseID) } else { return try await interactor.getLoadedCourseBlocks(courseID: courseID) } From b4fcf31493cceb200b02b92ca9ca3f44ae639db4 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 13:27:21 +0300 Subject: [PATCH 07/51] fix: Removed unused functions and hardcoded variables --- Core/Core/Configuration/Connectivity.swift | 3 +- .../Container/CourseContainerViewModel.swift | 29 +------------------ 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 741791b19..0a1a58ff8 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -28,6 +28,7 @@ public class Connectivity: ConnectivityProtocol { private let networkManager = NetworkReachabilityManager() private let verificationURL: URL private let verificationTimeout: TimeInterval + private let secondsPast: TimeInterval = 30 private static var lastVerificationDate: TimeInterval? private static var lastVerificationResult: Bool = false @@ -78,7 +79,7 @@ public class Connectivity: ConnectivityProtocol { case .reachable: let nowTS = Date().timeIntervalSince1970 if let lastTS = Connectivity.lastVerificationDate, - nowTS - lastTS < 30 { + nowTS - lastTS < self.secondsPast { self._isInternetAvailable = Connectivity.lastVerificationResult } else { Task { @MainActor in diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 31950d661..c6aa68584 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -110,8 +110,6 @@ public final class CourseContainerViewModel: BaseCourseViewModel { private let interactor: CourseInteractorProtocol private let authInteractor: AuthInteractorProtocol - private let timeOutIntervalSeconds: UInt64 = 15 - let analytics: CourseAnalytics let coreAnalytics: CoreAnalytics private(set) var storage: CourseStorage @@ -197,35 +195,10 @@ public final class CourseContainerViewModel: BaseCourseViewModel { ) } - private func getCourseBlocksWithTimeout( - courseID: String, - timeoutSeconds: UInt64 - ) async throws -> CourseStructure? { - return try await withThrowingTaskGroup(of: CourseStructure?.self) { group in - - group.addTask { - try await self.interactor.getCourseBlocks(courseID: courseID) - } - - group.addTask { - try await Task.sleep(nanoseconds: timeoutSeconds * 1_000_000_000) - return nil - } - - guard let firstResult = try await group.next() else { - return nil - } - - group.cancelAll() - - return firstResult - } - } - @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - return try await self.interactor.getCourseBlocks(courseID: courseID) + return try await interactor.getCourseBlocks(courseID: courseID) } else { return try await interactor.getLoadedCourseBlocks(courseID: courseID) } From 6121c2a63f27676232bf52b9f92559b6eb0dc841 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 13:50:29 +0300 Subject: [PATCH 08/51] fix: issue 581 --- Core/Core/Configuration/Connectivity.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 0a1a58ff8..d3f879aed 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -29,7 +29,6 @@ public class Connectivity: ConnectivityProtocol { private let verificationURL: URL private let verificationTimeout: TimeInterval private let secondsPast: TimeInterval = 30 - private static var lastVerificationDate: TimeInterval? private static var lastVerificationResult: Bool = false From b26c3d7de4554b4dadeaa55e07f0426204859c9c Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 15:15:30 +0300 Subject: [PATCH 09/51] fix: progress on issue 581 --- Core/Core/Configuration/Connectivity.swift | 87 ++++++++++--------- Core/Core/View/Base/OfflineSnackBarView.swift | 3 +- Core/Core/View/Base/WebBrowser.swift | 3 +- .../Container/CourseContainerView.swift | 4 +- .../Presentation/Dates/CourseDatesView.swift | 2 +- .../Presentation/Handouts/HandoutsView.swift | 2 +- .../Presentation/Offline/OfflineView.swift | 2 +- .../Subviews/LargestDownloadsView.swift | 2 +- .../Outline/CourseOutlineView.swift | 2 +- .../CourseVertical/CourseVerticalView.swift | 4 +- .../Subviews/CustomDisclosureGroup.swift | 2 +- .../Unit/CourseNavigationView.swift | 2 +- .../Presentation/Unit/CourseUnitView.swift | 2 +- .../Presentation/Unit/Subviews/WebView.swift | 2 +- .../Video/EncodedVideoPlayer.swift | 2 +- .../Presentation/Video/SubtitlesView.swift | 2 +- .../Video/YouTubeVideoPlayer.swift | 2 +- .../Presentation/AllCoursesView.swift | 2 +- .../Presentation/ListDashboardView.swift | 2 +- .../PrimaryCourseDashboardView.swift | 2 +- .../NativeDiscovery/CourseDetailsView.swift | 2 +- .../NativeDiscovery/DiscoveryView.swift | 2 +- .../NativeDiscovery/SearchView.swift | 2 +- .../WebPrograms/ProgramWebviewView.swift | 2 +- .../Presentation/AppDownloadsViewModel.swift | 2 +- OpenEdX/DI/AppAssembly.swift | 14 ++- .../DatesAndCalendar/CoursesToSyncView.swift | 2 +- .../DatesAndCalendarView.swift | 2 +- .../Elements/NewCalendarView.swift | 2 +- .../SyncCalendarOptionsView.swift | 2 +- .../DeleteAccount/DeleteAccountView.swift | 2 +- .../Presentation/Profile/ProfileView.swift | 5 +- .../Settings/ManageAccountView.swift | 6 +- .../Presentation/Settings/SettingsView.swift | 2 +- .../Settings/VideoQualityView.swift | 2 +- .../Settings/VideoSettingsView.swift | 2 +- .../Settings/SettingsViewModelTests.swift | 12 +-- 37 files changed, 107 insertions(+), 87 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index d3f879aed..2d1df1d34 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -1,9 +1,9 @@ -//// -//// Connectivity.swift -//// OpenEdX -//// -//// Created by  Stepanok Ivan on 15.12.2022. -//// +// +// Connectivity.swift +// OpenEdX +// +// Created by Stepanok Ivan on 15.12.2022. +// import Alamofire import Combine @@ -14,7 +14,7 @@ public enum InternetState: Sendable { case notReachable } -//sourcery: AutoMockable +// sourcery: AutoMockable @MainActor public protocol ConnectivityProtocol: Sendable { var isInternetAvaliable: Bool { get } @@ -29,69 +29,73 @@ public class Connectivity: ConnectivityProtocol { private let verificationURL: URL private let verificationTimeout: TimeInterval private let secondsPast: TimeInterval = 30 + private static var lastVerificationDate: TimeInterval? private static var lastVerificationResult: Bool = false private var _isInternetAvailable: Bool = true { - didSet { - internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) - } - } + didSet { + internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) + } + } - public var isInternetAvaliable: Bool { - return _isInternetAvailable - } + public var isInternetAvaliable: Bool { + _isInternetAvailable + } public var isMobileData: Bool { - if let networkManager { - return networkManager.isReachableOnCellular - } else { - return false - } + networkManager?.isReachableOnCellular == true } public let internetReachableSubject = CurrentValueSubject(nil) public init( - urlToVerify: URL = Config().baseURL, + config: ConfigProtocol, timeout: TimeInterval = 15 ) { - self.verificationURL = urlToVerify + print("+++ go") + self.verificationURL = config.baseURL self.verificationTimeout = timeout checkInternet() } deinit { + print("+++ deinit") networkManager?.stopListening() } - private func checkInternet() { - guard let nm = networkManager else { - _isInternetAvailable = false - return - } + @MainActor + private func updateAvailability( + _ available: Bool, + lastChecked: TimeInterval = Date().timeIntervalSince1970 + ) { + self._isInternetAvailable = available + Connectivity.lastVerificationDate = lastChecked + Connectivity.lastVerificationResult = available + } - nm.startListening { [weak self] status in - DispatchQueue.main.async { - guard let self = self else { return } + func checkInternet() { + networkManager?.startListening(onQueue: .global()) { [weak self] status in + guard let self = self else { return } + let now = Date().timeIntervalSince1970 + + Task { @MainActor in switch status { case .reachable: - let nowTS = Date().timeIntervalSince1970 - if let lastTS = Connectivity.lastVerificationDate, - nowTS - lastTS < self.secondsPast { - self._isInternetAvailable = Connectivity.lastVerificationResult + if let last = Connectivity.lastVerificationDate, + now - last < self.secondsPast { + print("+++ last") + self.updateAvailability(Connectivity.lastVerificationResult) } else { - Task { @MainActor in + Task.detached { + print("+++ verif") let live = await self.verifyInternet() - Connectivity.lastVerificationDate = Date().timeIntervalSince1970 - Connectivity.lastVerificationResult = live - self._isInternetAvailable = live + await self.updateAvailability(live, lastChecked: Date().timeIntervalSince1970) } } + case .notReachable, .unknown: - Connectivity.lastVerificationDate = nil - Connectivity.lastVerificationResult = false - self._isInternetAvailable = false + self.updateAvailability(false, lastChecked: 0) } } } @@ -106,11 +110,14 @@ public class Connectivity: ConnectivityProtocol { let (_, response) = try await URLSession.shared.data(for: request) if let http = response as? HTTPURLResponse, (200..<400).contains(http.statusCode) { + print("++++ got response") return true } } catch { + print("++++ no response") return false } + print("++++ no response") return false } } diff --git a/Core/Core/View/Base/OfflineSnackBarView.swift b/Core/Core/View/Base/OfflineSnackBarView.swift index c26a577ee..ad6c70130 100644 --- a/Core/Core/View/Base/OfflineSnackBarView.swift +++ b/Core/Core/View/Base/OfflineSnackBarView.swift @@ -79,6 +79,7 @@ public struct OfflineSnackBarView: View { struct OfflineSnackBarView_Previews: PreviewProvider { static var previews: some View { - OfflineSnackBarView(connectivity: Connectivity(), reloadAction: {}) + let configMock = ConfigMock() + OfflineSnackBarView(connectivity: Connectivity(config: configMock), reloadAction: {}) } } diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift index cd61dcdb6..6ae177cb4 100644 --- a/Core/Core/View/Base/WebBrowser.swift +++ b/Core/Core/View/Base/WebBrowser.swift @@ -81,6 +81,7 @@ public struct WebBrowser: View { struct WebBrowser_Previews: PreviewProvider { static var previews: some View { - WebBrowser(url: "", pageTitle: "", connectivity: Connectivity()) + let configMock = ConfigMock() + WebBrowser(url: "", pageTitle: "", connectivity: Connectivity(config: configMock)) } } diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index cc2bdc9e2..8db22ae10 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -366,7 +366,7 @@ struct CourseScreensView_Previews: PreviewProvider { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, @@ -382,7 +382,7 @@ struct CourseScreensView_Previews: PreviewProvider { interactor: CourseInteractor.mock, router: CourseRouterMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), config: ConfigMock(), courseID: "1", courseName: "a", diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift index 192b7c2a9..5de224b44 100644 --- a/Course/Course/Presentation/Dates/CourseDatesView.swift +++ b/Course/Course/Presentation/Dates/CourseDatesView.swift @@ -196,7 +196,7 @@ struct CourseDatesView_Previews: PreviewProvider { interactor: CourseInteractor(repository: CourseRepositoryMock()), router: CourseRouterMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), config: ConfigMock(), courseID: "", courseName: "", diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index d04be11af..82d74a408 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -123,7 +123,7 @@ struct HandoutsView_Previews: PreviewProvider { let viewModel = HandoutsViewModel(interactor: CourseInteractor.mock, router: CourseRouterMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), courseID: "", analytics: CourseAnalyticsMock()) HandoutsView( diff --git a/Course/Course/Presentation/Offline/OfflineView.swift b/Course/Course/Presentation/Offline/OfflineView.swift index 9b5cee5ed..168e9e574 100644 --- a/Course/Course/Presentation/Offline/OfflineView.swift +++ b/Course/Course/Presentation/Offline/OfflineView.swift @@ -249,7 +249,7 @@ struct OfflineView: View { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift b/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift index 7aa0930d7..3c13cd278 100644 --- a/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift +++ b/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift @@ -93,7 +93,7 @@ struct LargestDownloadsView_Previews: PreviewProvider { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 8872771a2..4049276ff 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -336,7 +336,7 @@ struct CourseOutlineView_Previews: PreviewProvider { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift index ed7b60af4..364e9c00a 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift @@ -181,9 +181,9 @@ struct CourseVerticalView_Previews: PreviewProvider { sequentialIndex: 0, router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()), ) - + return Group { CourseVerticalView( title: "Course title", diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index ff252a654..edba4603e 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -399,7 +399,7 @@ struct CustomDisclosureGroup_Previews: PreviewProvider { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Unit/CourseNavigationView.swift b/Course/Course/Presentation/Unit/CourseNavigationView.swift index 4257f18e9..4047f81c7 100644 --- a/Course/Course/Presentation/Unit/CourseNavigationView.swift +++ b/Course/Course/Presentation/Unit/CourseNavigationView.swift @@ -160,7 +160,7 @@ struct CourseNavigationView_Previews: PreviewProvider { config: ConfigMock(), router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), storage: CourseStorageMock(), manager: DownloadManagerMock() ) diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 2588d7369..2725afd2e 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -723,7 +723,7 @@ struct CourseUnitView_Previews: PreviewProvider { config: ConfigMock(), router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), storage: CourseStorageMock(), manager: DownloadManagerMock() )) diff --git a/Course/Course/Presentation/Unit/Subviews/WebView.swift b/Course/Course/Presentation/Unit/Subviews/WebView.swift index b8823bda6..935884a28 100644 --- a/Course/Course/Presentation/Unit/Subviews/WebView.swift +++ b/Course/Course/Presentation/Unit/Subviews/WebView.swift @@ -23,7 +23,7 @@ struct WebView: View { url: url, dataUrl: localUrl, viewModel: Container.shared.resolve(WebUnitViewModel.self)!, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), injections: injections, blockID: blockID ) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index a2d69340a..52e2428f1 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -145,7 +145,7 @@ struct EncodedVideoPlayer_Previews: PreviewProvider { viewModel: EncodedVideoPlayerViewModel( languages: [], playerStateSubject: CurrentValueSubject(nil), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), playerHolder: PlayerViewControllerHolder.mock, appStorage: CoreStorageMock(), analytics: CourseAnalyticsMock() diff --git a/Course/Course/Presentation/Video/SubtitlesView.swift b/Course/Course/Presentation/Video/SubtitlesView.swift index 5f7efc8a6..1680f2191 100644 --- a/Course/Course/Presentation/Video/SubtitlesView.swift +++ b/Course/Course/Presentation/Video/SubtitlesView.swift @@ -124,7 +124,7 @@ struct SubtittlesView_Previews: PreviewProvider { viewModel: VideoPlayerViewModel( languages: [], playerStateSubject: CurrentValueSubject(nil), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), playerHolder: PlayerViewControllerHolder.mock, appStorage: CoreStorageMock(), analytics: CourseAnalyticsMock() diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index 5914a1807..8f5e2d14f 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -88,7 +88,7 @@ struct YouTubeVideoPlayer_Previews: PreviewProvider { viewModel: YouTubeVideoPlayerViewModel( languages: [], playerStateSubject: CurrentValueSubject(nil), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), playerHolder: YoutubePlayerViewControllerHolder.mock, appStorage: CoreStorageMock(), analytics: CourseAnalyticsMock() diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index 4ab5fd905..8b8e77acc 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -205,7 +205,7 @@ struct AllCoursesView_Previews: PreviewProvider { static var previews: some View { let vm = AllCoursesViewModel( interactor: DashboardInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: DashboardAnalyticsMock(), storage: CoreStorageMock() ) diff --git a/Dashboard/Dashboard/Presentation/ListDashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift index 8f668dcc5..ac397f6d6 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -163,7 +163,7 @@ struct ListDashboardView_Previews: PreviewProvider { static var previews: some View { let vm = ListDashboardViewModel( interactor: DashboardInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: DashboardAnalyticsMock(), storage: CoreStorageMock() ) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 905dcaaae..e0f6eff05 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -334,7 +334,7 @@ struct PrimaryCourseDashboardView_Previews: PreviewProvider { static var previews: some View { let vm = PrimaryCourseDashboardViewModel( interactor: DashboardInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: DashboardAnalyticsMock(), config: ConfigMock(), storage: CoreStorageMock(), diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift index 2cbe294c1..fe1c32657 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift @@ -439,7 +439,7 @@ struct CourseDetailsView_Previews: PreviewProvider { analytics: DiscoveryAnalyticsMock(), config: ConfigMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), storage: CoreStorageMock() ) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index ffeb3451b..cafe26adb 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -211,7 +211,7 @@ struct DiscoveryView_Previews: PreviewProvider { let vm = DiscoveryViewModel(router: DiscoveryRouterMock(), config: ConfigMock(), interactor: DiscoveryInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: DiscoveryAnalyticsMock(), storage: CoreStorageMock()) let router = DiscoveryRouterMock() diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift index c059dbcba..7c565bfad 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift @@ -222,7 +222,7 @@ struct SearchView_Previews: PreviewProvider { let router = DiscoveryRouterMock() let vm = SearchViewModel( interactor: DiscoveryInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), router: router, analytics: DiscoveryAnalyticsMock(), storage: CoreStorageMock(), diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift index a206303d8..dbfc60403 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift @@ -139,7 +139,7 @@ struct ProgramWebviewView_Previews: PreviewProvider { router: DiscoveryRouterMock(), config: ConfigMock(), interactor: DiscoveryInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: DiscoveryAnalyticsMock(), authInteractor: AuthInteractor.mock ), diff --git a/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift b/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift index 104efc6f7..79696e030 100644 --- a/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift +++ b/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift @@ -779,7 +779,7 @@ public extension AppDownloadsViewModel { interactor: DownloadsInteractor.mock, courseManager: CourseStructureManagerMock(), downloadManager: DownloadManagerMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), downloadsHelper: DownloadsHelperMock(), router: DownloadsRouterMock(), storage: DownloadsStorageMock(), diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index f90b6a500..840cce6d5 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -87,8 +87,8 @@ class AppAssembly: Assembly { r.resolve(AnalyticsManager.self)! }.inObjectScope(.container) - container.register(ConnectivityProtocol.self) { @MainActor _ in - Connectivity() + container.register(ConnectivityProtocol.self) { @MainActor r in + Connectivity(config: r.resolve(ConfigProtocol.self)!) } container.register(DatabaseManager.self) { _ in @@ -193,7 +193,7 @@ class AppAssembly: Assembly { keychain: r.resolve(KeychainSwift.self)! ) } - + container.register(Validator.self) { _ in Validator() }.inObjectScope(.container) @@ -235,6 +235,14 @@ class AppAssembly: Assembly { courseDropDownNavigationEnabled: config.uiComponents.courseDropDownNavigationEnabled ) }.inObjectScope(.container) + + container.register(ConnectivityProtocol.self) { @MainActor r in + Connectivity( + config: r.resolve(ConfigProtocol.self)!, + timeout: 15 + ) + } + .inObjectScope(.container) } } // swiftlint:enable function_body_length diff --git a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift index ede274a85..e0791ac31 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift @@ -158,7 +158,7 @@ struct CoursesToSyncView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()), ) return CoursesToSyncView(viewModel: vm) .previewDisplayName("Courses to Sync") diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift index 52ef209f9..3993b8580 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift @@ -188,7 +188,7 @@ struct DatesAndCalendarView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()), ) DatesAndCalendarView(viewModel: vm) .loadFonts() diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift index 9d8b9dd70..ed4e27b8a 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift @@ -167,7 +167,7 @@ struct NewCalendarView: View { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()) ), beginSyncingTapped: { }, diff --git a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift index b5aa1e691..e96a0cc1b 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift @@ -271,7 +271,7 @@ struct SyncCalendarOptionsView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()), ) SyncCalendarOptionsView(viewModel: vm) .loadFonts() diff --git a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift index 585c06279..9b2ad9525 100644 --- a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift +++ b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift @@ -188,7 +188,7 @@ struct DeleteAccountView_Previews: PreviewProvider { let vm = DeleteAccountViewModel( interactor: ProfileInteractor.mock, router: router, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: ProfileAnalyticsMock() ) diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index 1691afc3d..b9d49b4b8 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -189,12 +189,13 @@ public struct ProfileView: View { struct ProfileView_Previews: PreviewProvider { static var previews: some View { let router = ProfileRouterMock() + let config = ConfigMock() let vm = ProfileViewModel( interactor: ProfileInteractor.mock, router: router, analytics: ProfileAnalyticsMock(), - config: ConfigMock(), - connectivity: Connectivity() + config: config, + connectivity: Connectivity(config: config), ) ProfileView(viewModel: vm) diff --git a/Profile/Profile/Presentation/Settings/ManageAccountView.swift b/Profile/Profile/Presentation/Settings/ManageAccountView.swift index 8b791d42b..154eddbcf 100644 --- a/Profile/Profile/Presentation/Settings/ManageAccountView.swift +++ b/Profile/Profile/Presentation/Settings/ManageAccountView.swift @@ -201,11 +201,13 @@ public struct ManageAccountView: View { struct ManageAccountView_Previews: PreviewProvider { static var previews: some View { let router = ProfileRouterMock() + let configMock = ConfigMock() + let vm = ManageAccountViewModel( router: router, analytics: ProfileAnalyticsMock(), - config: ConfigMock(), - connectivity: Connectivity(), + config: configMock, + connectivity: Connectivity(config: configMock), interactor: ProfileInteractor.mock ) diff --git a/Profile/Profile/Presentation/Settings/SettingsView.swift b/Profile/Profile/Presentation/Settings/SettingsView.swift index e58533f4d..b1410b656 100644 --- a/Profile/Profile/Presentation/Settings/SettingsView.swift +++ b/Profile/Profile/Presentation/Settings/SettingsView.swift @@ -254,7 +254,7 @@ public struct SettingsView: View { coreAnalytics: CoreAnalyticsMock(), config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: CoreStorageMock() ) diff --git a/Profile/Profile/Presentation/Settings/VideoQualityView.swift b/Profile/Profile/Presentation/Settings/VideoQualityView.swift index d06c1ea9f..8f662976d 100644 --- a/Profile/Profile/Presentation/Settings/VideoQualityView.swift +++ b/Profile/Profile/Presentation/Settings/VideoQualityView.swift @@ -134,7 +134,7 @@ public struct VideoQualityView: View { coreAnalytics: CoreAnalyticsMock(), config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: CoreStorageMock() ) diff --git a/Profile/Profile/Presentation/Settings/VideoSettingsView.swift b/Profile/Profile/Presentation/Settings/VideoSettingsView.swift index 3e4fd4b4c..a8c29bcc0 100644 --- a/Profile/Profile/Presentation/Settings/VideoSettingsView.swift +++ b/Profile/Profile/Presentation/Settings/VideoSettingsView.swift @@ -141,7 +141,7 @@ public struct VideoSettingsView: View { coreAnalytics: CoreAnalyticsMock(), config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: CoreStorageMock() ) diff --git a/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift b/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift index d9e80e66f..3455d8814 100644 --- a/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift @@ -44,7 +44,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) @@ -82,7 +82,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) @@ -119,7 +119,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) @@ -156,7 +156,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) @@ -193,7 +193,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) @@ -230,7 +230,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) From f21a460b481ccd60200686f66e8007ef67bcbc80 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 15:47:07 +0300 Subject: [PATCH 10/51] fix: apply updates --- Core/Core/Configuration/Connectivity.swift | 7 ------- OpenEdX/DI/AppAssembly.swift | 12 ++++++------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 2d1df1d34..25ecf63e6 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -53,14 +53,12 @@ public class Connectivity: ConnectivityProtocol { config: ConfigProtocol, timeout: TimeInterval = 15 ) { - print("+++ go") self.verificationURL = config.baseURL self.verificationTimeout = timeout checkInternet() } deinit { - print("+++ deinit") networkManager?.stopListening() } @@ -84,11 +82,9 @@ public class Connectivity: ConnectivityProtocol { case .reachable: if let last = Connectivity.lastVerificationDate, now - last < self.secondsPast { - print("+++ last") self.updateAvailability(Connectivity.lastVerificationResult) } else { Task.detached { - print("+++ verif") let live = await self.verifyInternet() await self.updateAvailability(live, lastChecked: Date().timeIntervalSince1970) } @@ -110,14 +106,11 @@ public class Connectivity: ConnectivityProtocol { let (_, response) = try await URLSession.shared.data(for: request) if let http = response as? HTTPURLResponse, (200..<400).contains(http.statusCode) { - print("++++ got response") return true } } catch { - print("++++ no response") return false } - print("++++ no response") return false } } diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 840cce6d5..9dc64cf92 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -86,11 +86,12 @@ class AppAssembly: Assembly { container.register(DownloadsAnalytics.self) { r in r.resolve(AnalyticsManager.self)! }.inObjectScope(.container) - - container.register(ConnectivityProtocol.self) { @MainActor r in - Connectivity(config: r.resolve(ConfigProtocol.self)!) - } - + +// container.register(ConnectivityProtocol.self) { @MainActor r in +// Connectivity(config: r.resolve(ConfigProtocol.self)!) +// } +// + container.register(DatabaseManager.self) { _ in DatabaseManager(databaseName: "Database") }.inObjectScope(.container) @@ -242,7 +243,6 @@ class AppAssembly: Assembly { timeout: 15 ) } - .inObjectScope(.container) } } // swiftlint:enable function_body_length From 327bb30268435e0cb10e10308fcee25426ae0ff3 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 18:37:53 +0300 Subject: [PATCH 11/51] fix: connectivity logic update --- Core/Core/Configuration/Connectivity.swift | 80 ++++++++++------------ OpenEdX/DI/AppAssembly.swift | 7 +- 2 files changed, 39 insertions(+), 48 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 25ecf63e6..4349f4d0c 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -22,90 +22,86 @@ public protocol ConnectivityProtocol: Sendable { var internetReachableSubject: CurrentValueSubject { get } } -@MainActor public class Connectivity: ConnectivityProtocol { private let networkManager = NetworkReachabilityManager() private let verificationURL: URL private let verificationTimeout: TimeInterval - private let secondsPast: TimeInterval = 30 + private let cacheValidity: TimeInterval = 5//30 + + private var lastVerificationDate: TimeInterval? + private var lastVerificationResult: Bool = true - private static var lastVerificationDate: TimeInterval? - private static var lastVerificationResult: Bool = false + public let internetReachableSubject = CurrentValueSubject(nil) - private var _isInternetAvailable: Bool = true { + private(set) var _isInternetAvailable: Bool = true { didSet { - internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) + Task { @MainActor in + internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) + } } } public var isInternetAvaliable: Bool { - _isInternetAvailable + if let last = lastVerificationDate, + Date().timeIntervalSince1970 - last < cacheValidity { + return lastVerificationResult + } + + Task { + await performVerification() + } + + return lastVerificationResult } public var isMobileData: Bool { networkManager?.isReachableOnCellular == true } - public let internetReachableSubject = CurrentValueSubject(nil) - public init( config: ConfigProtocol, timeout: TimeInterval = 15 ) { self.verificationURL = config.baseURL self.verificationTimeout = timeout - checkInternet() - } - - deinit { - networkManager?.stopListening() - } - - @MainActor - private func updateAvailability( - _ available: Bool, - lastChecked: TimeInterval = Date().timeIntervalSince1970 - ) { - self._isInternetAvailable = available - Connectivity.lastVerificationDate = lastChecked - Connectivity.lastVerificationResult = available - } - func checkInternet() { networkManager?.startListening(onQueue: .global()) { [weak self] status in guard let self = self else { return } - let now = Date().timeIntervalSince1970 - Task { @MainActor in switch status { case .reachable: - if let last = Connectivity.lastVerificationDate, - now - last < self.secondsPast { - self.updateAvailability(Connectivity.lastVerificationResult) - } else { - Task.detached { - let live = await self.verifyInternet() - await self.updateAvailability(live, lastChecked: Date().timeIntervalSince1970) - } - } - + await self.performVerification() case .notReachable, .unknown: - self.updateAvailability(false, lastChecked: 0) + self.updateAvailability(false, at: 0) } } } } + deinit { + networkManager?.stopListening() + } + + private func performVerification() async { + let now = Date().timeIntervalSince1970 + let live = await verifyInternet() + updateAvailability(live, at: now) + } + + private func updateAvailability(_ available: Bool, at timestamp: TimeInterval) { + _isInternetAvailable = available + lastVerificationDate = timestamp + lastVerificationResult = available + } + private func verifyInternet() async -> Bool { var request = URLRequest(url: verificationURL) request.httpMethod = "HEAD" request.timeoutInterval = verificationTimeout - do { let (_, response) = try await URLSession.shared.data(for: request) - if let http = response as? HTTPURLResponse, - (200..<400).contains(http.statusCode) { + if let http = response as? HTTPURLResponse, (200..<400).contains(http.statusCode) { return true } } catch { diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 9dc64cf92..93bb9cb52 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -87,11 +87,6 @@ class AppAssembly: Assembly { r.resolve(AnalyticsManager.self)! }.inObjectScope(.container) -// container.register(ConnectivityProtocol.self) { @MainActor r in -// Connectivity(config: r.resolve(ConfigProtocol.self)!) -// } -// - container.register(DatabaseManager.self) { _ in DatabaseManager(databaseName: "Database") }.inObjectScope(.container) @@ -242,7 +237,7 @@ class AppAssembly: Assembly { config: r.resolve(ConfigProtocol.self)!, timeout: 15 ) - } + }.inObjectScope(.container) } } // swiftlint:enable function_body_length From a9074d0ef59cb935ebcc97d5da9da21340d49fc9 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 18:39:29 +0300 Subject: [PATCH 12/51] fix: cacheValidity change --- Core/Core/Configuration/Connectivity.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 4349f4d0c..1168c41e1 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -27,7 +27,7 @@ public class Connectivity: ConnectivityProtocol { private let networkManager = NetworkReachabilityManager() private let verificationURL: URL private let verificationTimeout: TimeInterval - private let cacheValidity: TimeInterval = 5//30 + private let cacheValidity: TimeInterval = 30 private var lastVerificationDate: TimeInterval? private var lastVerificationResult: Bool = true From dabc7441331e29fefebb89e91370ee1cbe74ed51 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 6 Aug 2025 15:44:53 +0300 Subject: [PATCH 13/51] fix: unit tests update --- .../DiscoveryViewModelTests.swift | 8 +-- .../Presentation/SearchViewModelTests.swift | 8 +-- OpenEdX.xcodeproj/project.pbxproj | 60 +++++++++---------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift index f9918498d..8b9beb780 100644 --- a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift @@ -25,7 +25,7 @@ final class DiscoveryViewModelTests: XCTestCase { func testGetDiscoveryCourses() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), config: ConfigMock(), @@ -81,7 +81,7 @@ final class DiscoveryViewModelTests: XCTestCase { func testDiscoverySuccess() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), config: ConfigMock(), @@ -191,7 +191,7 @@ final class DiscoveryViewModelTests: XCTestCase { func testDiscoveryNoInternetError() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), config: ConfigMock(), @@ -215,7 +215,7 @@ final class DiscoveryViewModelTests: XCTestCase { func testDiscoveryUnknownError() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), config: ConfigMock(), diff --git a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift index a1851efcc..d837f982c 100644 --- a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift @@ -25,7 +25,7 @@ final class SearchViewModelTests: XCTestCase { func testSearchSuccess() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( @@ -87,7 +87,7 @@ final class SearchViewModelTests: XCTestCase { func testSearchEmptyQuerySuccess() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( @@ -111,7 +111,7 @@ final class SearchViewModelTests: XCTestCase { func testSearchNoInternetError() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( @@ -143,7 +143,7 @@ final class SearchViewModelTests: XCTestCase { func testSearchUnknownError() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 0faa4d76e..bc4acd68d 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -47,7 +47,7 @@ 07D5DA3528D075AA00752FD9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */; }; 07D5DA3E28D075AB00752FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */; }; 149FF39E2B9F1AB50034B33F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 149FF39C2B9F1AB50034B33F /* LaunchScreen.storyboard */; }; - 3ACA3A1E886F3F9B2735B9AF /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D27E034A83DDEBF18D53B04 /* Pods_App_OpenEdX.framework */; }; + 705A908842AAAFC361CD9D52 /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58FAA9E3ECC93D0E638D877D /* Pods_App_OpenEdX.framework */; }; A500668B2B613ED10024680B /* PushNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500668A2B613ED10024680B /* PushNotificationsManager.swift */; }; A500668D2B6143000024680B /* FCMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500668C2B6143000024680B /* FCMProvider.swift */; }; A50066932B614DCD0024680B /* FCMListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50066922B614DCD0024680B /* FCMListener.swift */; }; @@ -95,7 +95,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 005D1F4D92679D24B3BAA8FE /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; 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; }; @@ -136,9 +135,12 @@ 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 149FF39D2B9F1AB50034B33F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 3D27E034A83DDEBF18D53B04 /* Pods_App_OpenEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_OpenEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4449C4D4F119C87B452DDFCD /* Pods-App-OpenEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releaseprod.xcconfig"; sourceTree = ""; }; - 8A45E2C9AF0CBE70A09FB37B /* Pods-App-OpenEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugstage.xcconfig"; sourceTree = ""; }; + 23F5E05C0D7EC044B0C9E719 /* Pods-App-OpenEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugstage.xcconfig"; sourceTree = ""; }; + 2C04239322282B0E6963D56B /* Pods-App-OpenEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugdev.xcconfig"; sourceTree = ""; }; + 58FAA9E3ECC93D0E638D877D /* Pods_App_OpenEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_OpenEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6EAAEFD45AC766684492B1F7 /* Pods-App-OpenEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasestage.xcconfig"; sourceTree = ""; }; + 84185F0B853BA4F0A8C0217C /* Pods-App-OpenEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releaseprod.xcconfig"; sourceTree = ""; }; + 8A39EAD8663E6F16A59AF82E /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; A500668A2B613ED10024680B /* PushNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsManager.swift; sourceTree = ""; }; A500668C2B6143000024680B /* FCMProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FCMProvider.swift; sourceTree = ""; }; A50066922B614DCD0024680B /* FCMListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FCMListener.swift; sourceTree = ""; }; @@ -147,14 +149,12 @@ A59568962B61653700ED4F90 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; A59568982B616D9400ED4F90 /* PushLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushLink.swift; sourceTree = ""; }; BA7468752B96201D00793145 /* DeepLinkRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkRouter.swift; sourceTree = ""; }; - CCE1E0F850D3E25C0D6C6702 /* Pods-App-OpenEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugdev.xcconfig"; sourceTree = ""; }; CE1D5B7A2CE60E000019CA34 /* ContainerMainActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerMainActor.swift; sourceTree = ""; }; CE3BD14D2CBEB0DA0026F4E3 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = ""; }; CEB36E512D6A29CE00907A89 /* Downloads.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Downloads.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CEE5EDED2D6E0A290089F67C /* DownloadsPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsPersistence.swift; sourceTree = ""; }; - D70D30110012B7D52D05E876 /* Pods-App-OpenEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugprod.xcconfig"; sourceTree = ""; }; E0D6E6A22B1626B10089F9C9 /* Theme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Theme.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - FD0CE8D22B755B4003A113BB /* Pods-App-OpenEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasestage.xcconfig"; sourceTree = ""; }; + F6C9DA21C55F17F9F1F1D9A1 /* Pods-App-OpenEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugprod.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -176,7 +176,7 @@ 0219C67728F4347600D64452 /* Course.framework in Frameworks */, CEBA52772CEBB69100619E2B /* OEXFirebaseAnalytics in Frameworks */, 027DB33028D8A063002B6862 /* Dashboard.framework in Frameworks */, - 3ACA3A1E886F3F9B2735B9AF /* Pods_App_OpenEdX.framework in Frameworks */, + 705A908842AAAFC361CD9D52 /* Pods_App_OpenEdX.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -282,7 +282,7 @@ 072787B028D34D83002E9142 /* Discovery.framework */, 0770DE4A28D0A462006D8A5D /* Authorization.framework */, 0770DE1228D07845006D8A5D /* Core.framework */, - 3D27E034A83DDEBF18D53B04 /* Pods_App_OpenEdX.framework */, + 58FAA9E3ECC93D0E638D877D /* Pods_App_OpenEdX.framework */, ); name = Frameworks; sourceTree = ""; @@ -290,12 +290,12 @@ 55A895025FB07897BA68E063 /* Pods */ = { isa = PBXGroup; children = ( - D70D30110012B7D52D05E876 /* Pods-App-OpenEdX.debugprod.xcconfig */, - 8A45E2C9AF0CBE70A09FB37B /* Pods-App-OpenEdX.debugstage.xcconfig */, - CCE1E0F850D3E25C0D6C6702 /* Pods-App-OpenEdX.debugdev.xcconfig */, - 4449C4D4F119C87B452DDFCD /* Pods-App-OpenEdX.releaseprod.xcconfig */, - FD0CE8D22B755B4003A113BB /* Pods-App-OpenEdX.releasestage.xcconfig */, - 005D1F4D92679D24B3BAA8FE /* Pods-App-OpenEdX.releasedev.xcconfig */, + F6C9DA21C55F17F9F1F1D9A1 /* Pods-App-OpenEdX.debugprod.xcconfig */, + 23F5E05C0D7EC044B0C9E719 /* Pods-App-OpenEdX.debugstage.xcconfig */, + 2C04239322282B0E6963D56B /* Pods-App-OpenEdX.debugdev.xcconfig */, + 84185F0B853BA4F0A8C0217C /* Pods-App-OpenEdX.releaseprod.xcconfig */, + 6EAAEFD45AC766684492B1F7 /* Pods-App-OpenEdX.releasestage.xcconfig */, + 8A39EAD8663E6F16A59AF82E /* Pods-App-OpenEdX.releasedev.xcconfig */, ); path = Pods; sourceTree = ""; @@ -390,7 +390,7 @@ isa = PBXNativeTarget; buildConfigurationList = 07D5DA4528D075AB00752FD9 /* Build configuration list for PBXNativeTarget "OpenEdX" */; buildPhases = ( - B2F937DF587D9697AF13B3F9 /* [CP] Check Pods Manifest.lock */, + 8389A0AC71F15AC25A0DF8E0 /* [CP] Check Pods Manifest.lock */, 0770DE2328D08647006D8A5D /* SwiftLint */, 07D5DA2D28D075AA00752FD9 /* Sources */, 07D5DA2E28D075AA00752FD9 /* Frameworks */, @@ -511,7 +511,7 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; - B2F937DF587D9697AF13B3F9 /* [CP] Check Pods Manifest.lock */ = { + 8389A0AC71F15AC25A0DF8E0 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -694,14 +694,14 @@ }; 02DD1C9629E80CC200F35DCE /* DebugStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8A45E2C9AF0CBE70A09FB37B /* Pods-App-OpenEdX.debugstage.xcconfig */; + baseConfigurationReference = 23F5E05C0D7EC044B0C9E719 /* Pods-App-OpenEdX.debugstage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -786,14 +786,14 @@ }; 02DD1C9829E80CCB00F35DCE /* ReleaseStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FD0CE8D22B755B4003A113BB /* Pods-App-OpenEdX.releasestage.xcconfig */; + baseConfigurationReference = 6EAAEFD45AC766684492B1F7 /* Pods-App-OpenEdX.releasestage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -884,14 +884,14 @@ }; 0727875928D231FD002E9142 /* DebugDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = CCE1E0F850D3E25C0D6C6702 /* Pods-App-OpenEdX.debugdev.xcconfig */; + baseConfigurationReference = 2C04239322282B0E6963D56B /* Pods-App-OpenEdX.debugdev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -976,14 +976,14 @@ }; 0727875B28D23204002E9142 /* ReleaseDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 005D1F4D92679D24B3BAA8FE /* Pods-App-OpenEdX.releasedev.xcconfig */; + baseConfigurationReference = 8A39EAD8663E6F16A59AF82E /* Pods-App-OpenEdX.releasedev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -1128,14 +1128,14 @@ }; 07D5DA4628D075AB00752FD9 /* DebugProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D70D30110012B7D52D05E876 /* Pods-App-OpenEdX.debugprod.xcconfig */; + baseConfigurationReference = F6C9DA21C55F17F9F1F1D9A1 /* Pods-App-OpenEdX.debugprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -1166,14 +1166,14 @@ }; 07D5DA4728D075AB00752FD9 /* ReleaseProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4449C4D4F119C87B452DDFCD /* Pods-App-OpenEdX.releaseprod.xcconfig */; + baseConfigurationReference = 84185F0B853BA4F0A8C0217C /* Pods-App-OpenEdX.releaseprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; From 48d55bc69d0db8ea0b73c210482cbb320df002cb Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 6 Aug 2025 15:59:55 +0300 Subject: [PATCH 14/51] fix: removed team and coma --- OpenEdX.xcodeproj/project.pbxproj | 12 ++++++------ .../Profile/Presentation/Profile/ProfileView.swift | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index bc4acd68d..7cd19c72b 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -701,7 +701,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -793,7 +793,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -891,7 +891,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -983,7 +983,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -1135,7 +1135,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -1173,7 +1173,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index b9d49b4b8..b1c2e6c6d 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -195,9 +195,9 @@ struct ProfileView_Previews: PreviewProvider { router: router, analytics: ProfileAnalyticsMock(), config: config, - connectivity: Connectivity(config: config), + connectivity: Connectivity(config: config) ) - + ProfileView(viewModel: vm) .preferredColorScheme(.light) .previewDisplayName("DiscoveryView Light") From 786fa30b170a496783c40e3f57a23de163408106 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 6 Aug 2025 16:13:09 +0300 Subject: [PATCH 15/51] fix: removed extra comas --- .../Outline/CourseVertical/CourseVerticalView.swift | 2 +- .../Presentation/DatesAndCalendar/CoursesToSyncView.swift | 2 +- .../Presentation/DatesAndCalendar/DatesAndCalendarView.swift | 2 +- .../Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift index 364e9c00a..278215ea3 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift @@ -181,7 +181,7 @@ struct CourseVerticalView_Previews: PreviewProvider { sequentialIndex: 0, router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity(config: ConfigMock()), + connectivity: Connectivity(config: ConfigMock()) ) return Group { diff --git a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift index e0791ac31..f1aae91ef 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift @@ -158,7 +158,7 @@ struct CoursesToSyncView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity(config: ConfigMock()), + connectivity: Connectivity(config: ConfigMock()) ) return CoursesToSyncView(viewModel: vm) .previewDisplayName("Courses to Sync") diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift index 3993b8580..6539ea081 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift @@ -188,7 +188,7 @@ struct DatesAndCalendarView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity(config: ConfigMock()), + connectivity: Connectivity(config: ConfigMock()) ) DatesAndCalendarView(viewModel: vm) .loadFonts() diff --git a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift index e96a0cc1b..be28cc458 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift @@ -271,7 +271,7 @@ struct SyncCalendarOptionsView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity(config: ConfigMock()), + connectivity: Connectivity(config: ConfigMock()) ) SyncCalendarOptionsView(viewModel: vm) .loadFonts() From 0fce30be34b2c813a325b297bae3136ed9bed0f3 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 16 Sep 2025 11:55:36 +0300 Subject: [PATCH 16/51] fix: unit tests --- .../Course/Presentation/Container/CourseContainerView.swift | 6 +++--- .../Presentation/Container/CourseContainerViewModel.swift | 2 +- Course/Course/Presentation/Content/CourseContentView.swift | 2 +- .../Presentation/Content/Subviews/AllContentView.swift | 2 +- .../Presentation/Progress/CourseProgressScreenView.swift | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 73ea05091..8c8a2828b 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -382,7 +382,7 @@ public struct CourseContainerView: View { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, @@ -398,7 +398,7 @@ public struct CourseContainerView: View { interactor: CourseInteractor.mock, router: CourseRouterMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), config: ConfigMock(), courseID: "1", courseName: "a", @@ -409,7 +409,7 @@ public struct CourseContainerView: View { interactor: CourseInteractor.mock, router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()), ), courseID: "", title: "Title of Course", diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 2d1b2d051..7324dba43 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -1612,7 +1612,7 @@ extension CourseContainerViewModel { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Content/CourseContentView.swift b/Course/Course/Presentation/Content/CourseContentView.swift index d23590201..dbcca75c7 100644 --- a/Course/Course/Presentation/Content/CourseContentView.swift +++ b/Course/Course/Presentation/Content/CourseContentView.swift @@ -266,7 +266,7 @@ public struct CourseContentView: View { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Content/Subviews/AllContentView.swift b/Course/Course/Presentation/Content/Subviews/AllContentView.swift index 696586dd3..dc37c86b9 100644 --- a/Course/Course/Presentation/Content/Subviews/AllContentView.swift +++ b/Course/Course/Presentation/Content/Subviews/AllContentView.swift @@ -142,7 +142,7 @@ struct AllContentView: View { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Progress/CourseProgressScreenView.swift b/Course/Course/Presentation/Progress/CourseProgressScreenView.swift index 77341907d..7419a52ef 100644 --- a/Course/Course/Presentation/Progress/CourseProgressScreenView.swift +++ b/Course/Course/Presentation/Progress/CourseProgressScreenView.swift @@ -227,7 +227,7 @@ struct CourseProgressScreenView: View { interactor: CourseInteractor.mock, router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()) ) CourseProgressScreenView( @@ -236,7 +236,7 @@ struct CourseProgressScreenView: View { collapsed: .constant(false), viewHeight: .constant(0), viewModel: vm, - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()) ) .loadFonts() } From ad3f2608d28add66453fa343d97c25284f1bcc8f Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 29 Oct 2025 17:15:00 +0200 Subject: [PATCH 17/51] fix: removed state object warning --- .../Progress/CourseProgressScreenView.swift | 6 +++- default_config/dev/config.yaml | 35 +++---------------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/Course/Course/Presentation/Progress/CourseProgressScreenView.swift b/Course/Course/Presentation/Progress/CourseProgressScreenView.swift index 9a11ac964..3ee35119e 100644 --- a/Course/Course/Presentation/Progress/CourseProgressScreenView.swift +++ b/Course/Course/Presentation/Progress/CourseProgressScreenView.swift @@ -19,6 +19,7 @@ struct CourseProgressScreenView: View { @StateObject private var viewModel: CourseProgressViewModel + private let initialCourseStructure: CourseStructure? private let connectivity: ConnectivityProtocol @@ -37,7 +38,7 @@ struct CourseProgressScreenView: View { self._viewHeight = viewHeight self._viewModel = StateObject(wrappedValue: { viewModel }()) self.connectivity = connectivity - self.viewModel.courseStructure = courseStructure + self.initialCourseStructure = courseStructure } public var body: some View { @@ -111,6 +112,9 @@ struct CourseProgressScreenView: View { .ignoresSafeArea() ) .onFirstAppear { + if viewModel.courseStructure == nil { + viewModel.courseStructure = initialCourseStructure + } Task { await viewModel.getCourseProgress(courseID: courseID) } diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 9b2c7d64a..0634e3d75 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -1,33 +1,6 @@ -API_HOST_URL: 'https://axim-ccpv-dev.raccoongang.net' +API_HOST_URL: 'http://localhost:8000' SSO_URL: 'http://localhost:8000' -SSO_FINISHED_URL: 'http://localhost:8000' -ENVIRONMENT_DISPLAY_NAME: 'axim-ccpv-dev' +SSO_FINISHED_URL: 'http://localhost:8000' +ENVIRONMENT_DISPLAY_NAME: 'Localhost' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' -OAUTH_CLIENT_ID: 'SxDFlH1rb8Xw3nT6lOPwUejLb9vgXzOfkgqx1sY2' - -SSO_BUTTON_TITLE: - ar: "الدخول عبر SSO" - en: "Sign in with SSO" - -DISCOVERY: - TYPE: "native" - -FIREBASE: - ENABLED: true - ANALYTICS_SOURCE: "firebase" - CLOUD_MESSAGING_ENABLED: false - API_KEY: "AIzaSyCKAIXDLM7pnX43P_viTsfgbxrLBOaJwGo" - BUNDLE_ID: "com.raccoongang.NewEdX.stage" - CLIENT_ID: "156114692773-r5pgdcdjqq7sup75fdla4lk3q3kjc6m8.apps.googleusercontent.com" - GCM_SENDER_ID: "156114692773" - GOOGLE_APP_ID: "1:156114692773:ios:8058bca851a8bc7c187b4c" - PROJECT_ID: "openedxmobile-stage" - REVERSED_CLIENT_ID: "com.googleusercontent.apps.156114692773-r5pgdcdjqq7sup75fdla4lk3q3kjc6m8" - STORAGE_BUCKET: "openedxmobile-stage.appspot.com" - -UI_COMPONENTS: - COURSE_UNIT_PROGRESS_ENABLED: false - COURSE_NESTED_LIST_ENABLED: false - LOGIN_REGISTRATION_ENABLED: true - SAML_SSO_LOGIN_ENABLED: false - SAML_SSO_DEFAULT_LOGIN_BUTTON: false +OAUTH_CLIENT_ID: '' From 39ace76dab2b476e0b974823994eb914bab6f334 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 29 Oct 2025 18:06:51 +0200 Subject: [PATCH 18/51] fix: update config --- default_config/dev/config.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 0634e3d75..0d4e370e5 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -4,3 +4,18 @@ SSO_FINISHED_URL: 'http://localhost:8000' ENVIRONMENT_DISPLAY_NAME: 'Localhost' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' OAUTH_CLIENT_ID: '' + +SSO_BUTTON_TITLE: + ar: "الدخول عبر SSO" + en: "Sign in with SSO" + +EXPERIMENTAL_FEATURES: + APP_LEVEL_DOWNLOADS: + ENABLED: false + +UI_COMPONENTS: + COURSE_UNIT_PROGRESS_ENABLED: false + COURSE_NESTED_LIST_ENABLED: false + LOGIN_REGISTRATION_ENABLED: true + SAML_SSO_LOGIN_ENABLED: false + SAML_SSO_DEFAULT_LOGIN_BUTTON: false From 7fad8508ec425d906056ac8b0f3ed66115bf98ed Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 27 Nov 2025 07:43:50 +0200 Subject: [PATCH 19/51] feat: migration work in progess Migrated Course Unit View, CourseContainerViewModel, SignInViewModel, BaseCourseViewModel --- .../Presentation/Login/SignInView.swift | 1 - .../Presentation/Login/SignInViewModel.swift | 8 +-- .../Presentation/Startup/StartupView.swift | 23 ++++--- .../Startup/StartupViewModel.swift | 4 +- .../Container/BaseCourseViewModel.swift | 4 +- .../Container/CourseContainerView.swift | 3 +- .../Container/CourseContainerViewModel.swift | 61 ++++++++++--------- .../Content/CourseContentView.swift | 4 +- .../Content/Subviews/AllContentView.swift | 4 +- .../CourseOutlineAndProgressView.swift | 22 ++++--- .../Presentation/Offline/OfflineView.swift | 5 +- .../Subviews/LargestDownloadsView.swift | 4 +- .../Outline/CourseOutlineView.swift | 4 +- .../CourseAssignmentsCarouselSlideView.swift | 2 +- .../CourseGradeCarouselSlideView.swift | 2 +- .../CourseVideoCarouselSlideView.swift | 2 +- .../Subviews/CourseHeaderView.swift | 2 +- .../Unit/CourseNavigationView.swift | 1 - .../Presentation/Unit/CourseUnitView.swift | 2 +- .../Unit/CourseUnitViewModel.swift | 18 +++--- .../Subviews/LessonLineProgressView.swift | 2 +- .../Unit/Subviews/LessonProgressView.swift | 2 +- .../Unit/Subviews/VideoNavigationView.swift | 8 ++- 23 files changed, 94 insertions(+), 94 deletions(-) diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index f92309117..aeb19f3e4 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -18,7 +18,6 @@ public struct SignInView: View { @Environment(\.isHorizontal) private var isHorizontal - @ObservedObject private var viewModel: SignInViewModel public init(viewModel: SignInViewModel) { diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index 14bd27a92..659d1b2c8 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -16,11 +16,11 @@ import GoogleSignIn import MSAL @MainActor -public class SignInViewModel: ObservableObject { +@Observable public class SignInViewModel { - @Published private(set) var isShowProgress = false - @Published private(set) var showError: Bool = false - @Published private(set) var showAlert: Bool = false + private(set) var isShowProgress = false + private(set) var showError: Bool = false + private(set) var showAlert: Bool = false let sourceScreen: LogistrationSourceScreen var errorMessage: String? { diff --git a/Authorization/Authorization/Presentation/Startup/StartupView.swift b/Authorization/Authorization/Presentation/Startup/StartupView.swift index 32c73b673..7becf99b6 100644 --- a/Authorization/Authorization/Presentation/Startup/StartupView.swift +++ b/Authorization/Authorization/Presentation/Startup/StartupView.swift @@ -12,13 +12,12 @@ import Theme public struct StartupView: View { - @State private var searchQuery: String = "" +// @State private var searchQuery: String = "" @Environment(\.isHorizontal) private var isHorizontal - - @ObservedObject - private var viewModel: StartupViewModel - + + @Bindable private var viewModel: StartupViewModel + public init(viewModel: StartupViewModel) { self.viewModel = viewModel } @@ -56,13 +55,13 @@ public struct StartupView: View { .padding(.leading, 16) .padding(.top, 1) .foregroundColor(Theme.Colors.textInputTextColor) - TextField("", text: $searchQuery, onCommit: { - if searchQuery.isEmpty { return } + TextField("", text: $viewModel.searchQuery, onCommit: { + if viewModel.searchQuery.isEmpty { return } viewModel.router.showDiscoveryScreen( - searchQuery: searchQuery, + searchQuery: viewModel.searchQuery, sourceScreen: .startup ) - viewModel.logAnalytics(searchQuery: searchQuery) + viewModel.logAnalytics(searchQuery: viewModel.searchQuery) }) .autocapitalization(.none) .autocorrectionDisabled() @@ -80,14 +79,14 @@ public struct StartupView: View { .background( Theme.InputFieldBackground( placeHolder: AuthLocalization.Startup.searchPlaceholder, - text: searchQuery, + text: viewModel.searchQuery, padding: 48 ) ) Button { viewModel.router.showDiscoveryScreen( - searchQuery: searchQuery, + searchQuery: viewModel.searchQuery, sourceScreen: .startup ) viewModel.logAnalytics() @@ -120,7 +119,7 @@ public struct StartupView: View { .padding(.bottom, 2) } .onDisappear { - searchQuery = "" + viewModel.searchQuery = "" } .frameLimit() } diff --git a/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift b/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift index 68691ff69..1a5835acb 100644 --- a/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift +++ b/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift @@ -9,12 +9,12 @@ import Foundation import Core @MainActor -public class StartupViewModel: ObservableObject { +@Observable public class StartupViewModel { let router: AuthorizationRouter let analytics: CoreAnalytics let config: ConfigProtocol - @Published var searchQuery: String? + var searchQuery: String = "" public init( router: AuthorizationRouter, diff --git a/Course/Course/Presentation/Container/BaseCourseViewModel.swift b/Course/Course/Presentation/Container/BaseCourseViewModel.swift index cd1df40c7..f0b67b68c 100644 --- a/Course/Course/Presentation/Container/BaseCourseViewModel.swift +++ b/Course/Course/Presentation/Container/BaseCourseViewModel.swift @@ -11,8 +11,8 @@ import Core import Combine @MainActor -open class BaseCourseViewModel: ObservableObject { - +@Observable open class BaseCourseViewModel { + let manager: DownloadManagerProtocol var cancellables = Set() diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 99d433bed..3a5bb5335 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -14,8 +14,7 @@ import Theme public struct CourseContainerView: View { - @ObservedObject - public var viewModel: CourseContainerViewModel + @Bindable public var viewModel: CourseContainerViewModel @ObservedObject public var courseDatesViewModel: CourseDatesViewModel @ObservedObject diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 31fdc9292..6658a10a2 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -66,35 +66,35 @@ extension CourseTab { //swiftlint:disable type_body_length file_length @MainActor -public final class CourseContainerViewModel: BaseCourseViewModel { - - @Published public var selection: Int - @Published var selectedTab: ContentTab = .all - @Published var isShowProgress = false - @Published var isShowRefresh = false - @Published var courseStructure: CourseStructure? - @Published var courseDeadlineInfo: CourseDateBanner? - @Published var courseVideosStructure: CourseStructure? - @Published var courseAssignmentsStructure: CourseStructure? - @Published var courseProgressDetails: CourseProgressDetails? - @Published var showError: Bool = false - @Published var sequentialsDownloadState: [String: DownloadViewState] = [:] - @Published private(set) var downloadableVerticals: Set = [] - @Published var continueWith: ContinueWith? - @Published var userSettings: UserSettings? - @Published var isInternetAvaliable = true - @Published var dueDatesShifted: Bool = false - @Published var updateCourseProgress: Bool = false - @Published var totalFilesSize: Int = 1 - @Published var downloadedFilesSize: Int = 0 - @Published var largestDownloadBlocks: [CourseBlock] = [] - @Published var downloadAllButtonState: OfflineView.DownloadAllState = .start - @Published var expandedSections: [String: Bool] = [:] - @Published var courseDeadlines: CourseDates? - @Published private(set) var assignmentSectionsData: [AssignmentSection] = [] +@Observable public final class CourseContainerViewModel: BaseCourseViewModel { + + public var selection: Int + var selectedTab: ContentTab = .all + var isShowProgress = false + var isShowRefresh = false + var courseStructure: CourseStructure? + var courseDeadlineInfo: CourseDateBanner? + var courseVideosStructure: CourseStructure? + var courseAssignmentsStructure: CourseStructure? + var courseProgressDetails: CourseProgressDetails? + var showError: Bool = false + var sequentialsDownloadState: [String: DownloadViewState] = [:] + private(set) var downloadableVerticals: Set = [] + var continueWith: ContinueWith? + var userSettings: UserSettings? + var isInternetAvaliable = true + var dueDatesShifted: Bool = false + var updateCourseProgress: Bool = false + var totalFilesSize: Int = 1 + var downloadedFilesSize: Int = 0 + var largestDownloadBlocks: [CourseBlock] = [] + var downloadAllButtonState: OfflineView.DownloadAllState = .start + var expandedSections: [String: Bool] = [:] + var courseDeadlines: CourseDates? + private(set) var assignmentSectionsData: [AssignmentSection] = [] private(set) var realDownloadedFilesSize: Int = 0 - @Published var tabBarIndex = 0 + var tabBarIndex = 0 let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) @@ -149,6 +149,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel { selection: CourseTab = CourseTab.course, courseHelper: CourseDownloadHelperProtocol ) { + self.interactor = interactor self.authInteractor = authInteractor self.router = router @@ -167,8 +168,8 @@ public final class CourseContainerViewModel: BaseCourseViewModel { self.coreAnalytics = coreAnalytics self.selection = selection.rawValue self.courseHelper = courseHelper - self.courseHelper.videoQuality = storage.userSettings?.downloadQuality ?? .auto super.init(manager: manager) + self.courseHelper.videoQuality = storage.userSettings?.downloadQuality ?? .auto addObservers() } @@ -1273,7 +1274,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel { updateAssignmentSections() } - objectWillChange.send() +// objectWillChange.send() } @MainActor @@ -1290,7 +1291,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel { updateAssignmentSections() } - objectWillChange.send() +// objectWillChange.send() } private func updateBlockProgress( diff --git a/Course/Course/Presentation/Content/CourseContentView.swift b/Course/Course/Presentation/Content/CourseContentView.swift index 37f6d81b5..131bbc64d 100644 --- a/Course/Course/Presentation/Content/CourseContentView.swift +++ b/Course/Course/Presentation/Content/CourseContentView.swift @@ -14,7 +14,7 @@ import SwiftUIIntrospect public struct CourseContentView: View { - @StateObject private var viewModel: CourseContainerViewModel + @Bindable private var viewModel: CourseContainerViewModel private let title: String private let courseID: String @@ -67,7 +67,7 @@ public struct CourseContentView: View { viewHeight: Binding ) { self.title = title - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.courseID = courseID self._selection = selection self._coordinate = coordinate diff --git a/Course/Course/Presentation/Content/Subviews/AllContentView.swift b/Course/Course/Presentation/Content/Subviews/AllContentView.swift index dc37c86b9..d6d579bb1 100644 --- a/Course/Course/Presentation/Content/Subviews/AllContentView.swift +++ b/Course/Course/Presentation/Content/Subviews/AllContentView.swift @@ -13,7 +13,7 @@ import Theme struct AllContentView: View { - @StateObject private var viewModel: CourseContainerViewModel + @Bindable private var viewModel: CourseContainerViewModel private let title: String private let courseID: String private let dateTabIndex: Int @@ -32,7 +32,7 @@ struct AllContentView: View { dateTabIndex: Int ) { self.title = title - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.proxy = proxy self.courseID = courseID self.dateTabIndex = dateTabIndex diff --git a/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift b/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift index d12aacff4..57e7d623c 100644 --- a/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift +++ b/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift @@ -9,7 +9,7 @@ import WhatsNew public struct CourseOutlineAndProgressView: View { // MARK: - Variables - @StateObject private var viewModelContainer: CourseContainerViewModel + @Bindable private var viewModelContainer: CourseContainerViewModel @StateObject private var viewModelProgress: CourseProgressViewModel private let title: String private let courseID: String @@ -93,7 +93,7 @@ public struct CourseOutlineAndProgressView: View { connectivity: ConnectivityProtocol ) { self.title = title - self._viewModelContainer = StateObject(wrappedValue: { viewModelContainer }()) + self.viewModelContainer = viewModelContainer self._viewModelProgress = StateObject(wrappedValue: { viewModelProgress}()) self.courseID = courseID self.isVideo = isVideo @@ -108,13 +108,15 @@ public struct CourseOutlineAndProgressView: View { // MARK: - Body public var body: some View { ZStack(alignment: .top) { - if viewModelProgress.isLoading || viewModelContainer.isShowRefresh { - HStack(alignment: .center) { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 200) - .padding(.horizontal) - } - } else { + // MARK: - RETURN THIS! +// if viewModelProgress.isLoading || viewModelContainer.isShowRefresh { +// HStack(alignment: .center) { +// ProgressBar(size: 40, lineWidth: 8) +// .padding(.top, 200) +// .padding(.horizontal) +// } +// } else { + // MARK: - RETURN THIS! GeometryReader { _ in VStack(alignment: .center) { // MARK: - Page Body @@ -235,7 +237,7 @@ public struct CourseOutlineAndProgressView: View { } } .frameLimit() - } +// } } .background( Theme.Colors.background diff --git a/Course/Course/Presentation/Offline/OfflineView.swift b/Course/Course/Presentation/Offline/OfflineView.swift index 168e9e574..e795d858a 100644 --- a/Course/Course/Presentation/Offline/OfflineView.swift +++ b/Course/Course/Presentation/Offline/OfflineView.swift @@ -58,8 +58,7 @@ struct OfflineView: View { @Binding private var collapsed: Bool @Binding private var viewHeight: CGFloat - @StateObject - private var viewModel: CourseContainerViewModel + @Bindable private var viewModel: CourseContainerViewModel public init( courseID: String, @@ -72,7 +71,7 @@ struct OfflineView: View { self._coordinate = coordinate self._collapsed = collapsed self._viewHeight = viewHeight - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel } public var body: some View { diff --git a/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift b/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift index 3c13cd278..579a62ea3 100644 --- a/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift +++ b/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift @@ -12,8 +12,8 @@ import Theme public struct LargestDownloadsView: View { @State private var isEditing = false - @ObservedObject - private var viewModel: CourseContainerViewModel + + @Bindable private var viewModel: CourseContainerViewModel init(viewModel: CourseContainerViewModel) { self.viewModel = viewModel diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 4049276ff..745d26c54 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -14,7 +14,7 @@ import SwiftUIIntrospect public struct CourseOutlineView: View { - @StateObject private var viewModel: CourseContainerViewModel + @Bindable private var viewModel: CourseContainerViewModel private let title: String private let courseID: String private let isVideo: Bool @@ -46,7 +46,7 @@ public struct CourseOutlineView: View { dateTabIndex: Int ) { self.title = title - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.courseID = courseID self.isVideo = isVideo self._selection = selection diff --git a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseAssignmentsCarouselSlideView.swift b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseAssignmentsCarouselSlideView.swift index 1d83d6910..d11fda7fa 100644 --- a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseAssignmentsCarouselSlideView.swift +++ b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseAssignmentsCarouselSlideView.swift @@ -6,7 +6,7 @@ struct CourseAssignmentsCarouselSlideView: View { // MARK: - Variables @ObservedObject var viewModelProgress: CourseProgressViewModel - @ObservedObject var viewModelContainer: CourseContainerViewModel + var viewModelContainer: CourseContainerViewModel @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass diff --git a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseGradeCarouselSlideView.swift b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseGradeCarouselSlideView.swift index 1c2a2d03b..ea7feaa98 100644 --- a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseGradeCarouselSlideView.swift +++ b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseGradeCarouselSlideView.swift @@ -6,7 +6,7 @@ struct CourseGradeCarouselSlideView: View { // MARK: - Variables @ObservedObject var viewModelProgress: CourseProgressViewModel - @ObservedObject var viewModelContainer: CourseContainerViewModel + var viewModelContainer: CourseContainerViewModel // MARK: - Body var body: some View { diff --git a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift index b60b2262c..1fdae9155 100644 --- a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift +++ b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift @@ -6,7 +6,7 @@ struct CourseVideoCarouselSlideView: View { // MARK: - Variables @ObservedObject var viewModelProgress: CourseProgressViewModel - @ObservedObject var viewModelContainer: CourseContainerViewModel + var viewModelContainer: CourseContainerViewModel @State private var isHidingCompletedSections = true private var videoContentData: VideoContentData { diff --git a/Course/Course/Presentation/Subviews/CourseHeaderView.swift b/Course/Course/Presentation/Subviews/CourseHeaderView.swift index 3f604e7df..9cbfc2385 100644 --- a/Course/Course/Presentation/Subviews/CourseHeaderView.swift +++ b/Course/Course/Presentation/Subviews/CourseHeaderView.swift @@ -12,7 +12,7 @@ import Theme struct CourseHeaderView: View { - @ObservedObject var viewModel: CourseContainerViewModel + @Bindable var viewModel: CourseContainerViewModel private var title: String private var containerWidth: CGFloat private var animationNamespace: Namespace.ID diff --git a/Course/Course/Presentation/Unit/CourseNavigationView.swift b/Course/Course/Presentation/Unit/CourseNavigationView.swift index 2d25ec414..c20ae9ac1 100644 --- a/Course/Course/Presentation/Unit/CourseNavigationView.swift +++ b/Course/Course/Presentation/Unit/CourseNavigationView.swift @@ -11,7 +11,6 @@ import Combine struct CourseNavigationView: View { - @ObservedObject private var viewModel: CourseUnitViewModel private let playerStateSubject: CurrentValueSubject diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 9cc7e7fc9..11b3d08c9 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -15,7 +15,7 @@ import Theme public struct CourseUnitView: View { - @ObservedObject public var viewModel: CourseUnitViewModel + public var viewModel: CourseUnitViewModel @State private var showAlert: Bool = false @State var alertMessage: String? { didSet { diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index c832771a1..411690e77 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -120,8 +120,8 @@ public struct VerticalData: Equatable { } @MainActor -public final class CourseUnitViewModel: ObservableObject { - +@Observable public final class CourseUnitViewModel { + enum LessonAction: Sendable { case next case previous @@ -131,21 +131,21 @@ public final class CourseUnitViewModel: ObservableObject { var verticalIndex: Int var courseName: String - @Published var courseVideosStructure: CourseStructure? - @Published var index: Int = 0 + var courseVideosStructure: CourseStructure? + var index: Int = 0 var previousLesson: String = "" var nextLesson: String = "" - @Published var showError: Bool = false + var showError: Bool = false var errorMessage: String? { didSet { showError = errorMessage != nil } } - @Published public var allVideosForNavigation: [CourseBlock] = [] - @Published public var allVideosFetched = false - @Published public var isVideosForNavigationLoading: Bool = false - @Published var currentVideoIndex: Int? + public var allVideosForNavigation: [CourseBlock] = [] + public var allVideosFetched = false + public var isVideosForNavigationLoading: Bool = false + var currentVideoIndex: Int? var lessonID: String var courseID: String diff --git a/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift b/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift index 8233051d0..05f67d7bf 100644 --- a/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift +++ b/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift @@ -9,7 +9,7 @@ import SwiftUI import Theme struct LessonLineProgressView: View { - @ObservedObject var viewModel: CourseUnitViewModel + var viewModel: CourseUnitViewModel @Environment(\.isHorizontal) private var isHorizontal diff --git a/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift index 32badbf2a..9573f825f 100644 --- a/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift +++ b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift @@ -10,7 +10,7 @@ import Core import Theme struct LessonProgressView: View { - @ObservedObject var viewModel: CourseUnitViewModel + @Bindable var viewModel: CourseUnitViewModel @Environment(\.isHorizontal) private var isHorizontal diff --git a/Course/Course/Presentation/Unit/Subviews/VideoNavigationView.swift b/Course/Course/Presentation/Unit/Subviews/VideoNavigationView.swift index c65a50e6b..7b8c04473 100644 --- a/Course/Course/Presentation/Unit/Subviews/VideoNavigationView.swift +++ b/Course/Course/Presentation/Unit/Subviews/VideoNavigationView.swift @@ -5,11 +5,15 @@ import OEXFoundation import Theme struct VideoNavigationView: View { - @ObservedObject var viewModel: CourseUnitViewModel + @Bindable var viewModel: CourseUnitViewModel @Binding var currentBlock: CourseBlock? @State private var uiScrollView: UIScrollView? let block: CourseBlock + private var breadCrumps: String { + viewModel.createBreadCrumpsForVideoNavigation(video: block) + } + var body: some View { if viewModel.isVideosForNavigationLoading { HStack { @@ -36,8 +40,6 @@ struct VideoNavigationView: View { .padding(.bottom, 16) HStack { - let breadCrumps = viewModel.createBreadCrumpsForVideoNavigation(video: block) - VStack(alignment: .leading, spacing: 8) { Text(breadCrumps) .font(Theme.Fonts.bodySmall) From 5d76345423171d210b15bbe6843a75933d7c32cb Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 2 Dec 2025 05:54:07 +0200 Subject: [PATCH 20/51] feat: Migrates to new `@Observable` macro Swift, replacing `@ObservedObject` and `@StateObject`. This modernizes the data flow and simplifies the code by leveraging the compiler to automatically manage observation. Also it increases the minimum iOS deployment target to 17.0 --- .../Authorization.xcodeproj/project.pbxproj | 16 +-- .../Registration/SignUpView.swift | 16 +-- .../Registration/SignUpViewModel.swift | 34 ++--- .../Reset Password/ResetPasswordView.swift | 22 ++-- .../ResetPasswordViewModel.swift | 14 +- .../Presentation/SSO/SSOWebViewModel.swift | 9 +- .../SocialAuth/SocialAuthView.swift | 4 +- .../SocialAuth/SocialAuthViewModel.swift | 13 +- .../AvoidingHelpers/State/KeyboardState.swift | 3 +- .../State/KeyboardStateObserver.swift | 66 ++++++++-- .../KeyboardAvoidingModifier.swift | 50 +++---- .../View/Base/AppReview/AppReviewView.swift | 2 +- .../Base/AppReview/AppReviewViewModel.swift | 15 ++- .../Core/View/Base/BackNavigationButton.swift | 4 +- .../Base/BackNavigationButtonViewModel.swift | 5 +- Core/Core/View/Base/FieldConfiguration.swift | 11 +- Core/Core/View/Base/FileWebView.swift | 9 +- Core/Core/View/Base/PickerView.swift | 5 +- .../View/Base/RegistrationTextField.swift | 6 +- .../View/Base/VideoDownloadQualityView.swift | 14 +- Core/Core/View/Base/WebUnitView.swift | 6 +- Core/Core/View/Base/WebUnitViewModel.swift | 9 +- Core/Core/View/Base/Webview/WebView.swift | 9 +- Course/Course.xcodeproj/project.pbxproj | 24 ++-- .../Container/CourseContainerView.swift | 17 ++- .../Presentation/Dates/CourseDatesView.swift | 5 +- .../Dates/CourseDatesViewModel.swift | 13 +- .../Dates/Elements/CourseDateListView.swift | 4 +- .../Downloads/DownloadsView.swift | 11 +- .../Downloads/DownloadsViewModel.swift | 5 +- .../Presentation/Handouts/HandoutsView.swift | 5 +- .../Handouts/HandoutsViewModel.swift | 11 +- .../ CourseOutlineAndProgressViewModel.swift | 13 +- .../CourseOutlineAndProgressView.swift | 4 +- .../CourseVertical/CourseVerticalView.swift | 2 +- .../CourseVerticalViewModel.swift | 7 +- .../Progress/CourseProgressScreenView.swift | 6 +- .../Progress/CourseProgressViewModel.swift | 15 ++- .../CourseAssignmentsCarouselSlideView.swift | 2 +- .../CourseGradeCarouselSlideView.swift | 4 +- .../CourseVideoCarouselSlideView.swift | 2 +- .../CourseVideoDownloadBarView.swift | 13 +- .../CourseVideoDownloadBarViewModel.swift | 7 +- .../Video/EncodedVideoPlayer.swift | 7 +- .../Presentation/Video/SubtitlesView.swift | 1 - .../Video/VideoPlayerViewModel.swift | 19 +-- .../Video/YouTubeVideoPlayer.swift | 5 +- Dashboard/Dashboard.xcodeproj/project.pbxproj | 16 +-- .../Presentation/AllCoursesView.swift | 3 +- .../Presentation/AllCoursesViewModel.swift | 13 +- .../Presentation/ListDashboardView.swift | 5 +- .../Presentation/ListDashboardViewModel.swift | 9 +- .../PrimaryCourseDashboardView.swift | 4 +- .../PrimaryCourseDashboardViewModel.swift | 124 ++++++++++-------- Discovery/Discovery.xcodeproj/project.pbxproj | 16 +-- .../NativeDiscovery/CourseDetailsView.swift | 15 ++- .../CourseDetailsViewModel.swift | 12 +- .../NativeDiscovery/DiscoveryView.swift | 21 ++- .../NativeDiscovery/DiscoveryViewModel.swift | 11 +- .../NativeDiscovery/SearchView.swift | 13 +- .../NativeDiscovery/SearchViewModel.swift | 70 +++++----- .../WebDiscovery/DiscoveryWebview.swift | 21 ++- .../DiscoveryWebviewViewModel.swift | 14 +- .../WebPrograms/ProgramWebviewView.swift | 11 +- .../WebPrograms/ProgramWebviewViewModel.swift | 17 ++- .../Discussion.xcodeproj/project.pbxproj | 16 +-- .../Base/BaseResponsesViewModel.swift | 13 +- .../Comments/Responses/ResponsesView.swift | 2 +- .../Responses/ResponsesViewModel.swift | 5 +- .../Comments/Thread/ThreadView.swift | 2 +- .../Comments/Thread/ThreadViewModel.swift | 5 +- .../CreateNewThread/CreateNewThreadView.swift | 5 +- .../CreateNewThreadViewModel.swift | 11 +- .../DiscussionSearchTopicsView.swift | 13 +- .../DiscussionSearchTopicsViewModel.swift | 105 ++++++++------- .../DiscussionTopicsView.swift | 16 ++- .../DiscussionTopicsViewModel.swift | 20 +-- .../Presentation/Posts/PostsView.swift | 2 +- .../Presentation/Posts/PostsViewModel.swift | 18 +-- Downloads/Downloads.xcodeproj/project.pbxproj | 16 +-- .../Presentation/AppDownloadsView.swift | 6 +- .../Presentation/AppDownloadsViewModel.swift | 13 +- OpenEdX.xcodeproj/project.pbxproj | 12 +- OpenEdX/DI/ScreenAssembly.swift | 2 +- OpenEdX/Router.swift | 2 +- OpenEdX/View/MainScreenView.swift | 17 +-- OpenEdX/View/MainScreenViewModel.swift | 9 +- Podfile.lock | 2 +- Profile/Profile.xcodeproj/project.pbxproj | 16 +-- .../DatesAndCalendar/CoursesToSyncView.swift | 7 +- .../DatesAndCalendarView.swift | 4 +- .../DatesAndCalendarViewModel.swift | 65 ++++----- .../Elements/NewCalendarView.swift | 2 +- .../SyncCalendarOptionsView.swift | 9 +- .../DeleteAccount/DeleteAccountView.swift | 4 +- .../DeleteAccountViewModel.swift | 11 +- .../EditProfile/EditProfileView.swift | 19 ++- .../EditProfile/EditProfileViewModel.swift | 28 ++-- .../Presentation/Profile/ProfileView.swift | 4 +- .../Profile/ProfileViewModel.swift | 11 +- .../Subviews/ProfileSupportInfoView.swift | 2 +- .../Profile/UserProfile/UserProfileView.swift | 2 +- .../UserProfile/UserProfileViewModel.swift | 9 +- .../Settings/ManageAccountView.swift | 4 +- .../Settings/ManageAccountViewModel.swift | 11 +- .../Presentation/Settings/SettingsView.swift | 1 - .../Settings/SettingsViewModel.swift | 19 +-- .../Settings/VideoQualityView.swift | 1 - .../Settings/VideoSettingsView.swift | 3 +- WhatsNew/WhatsNew.xcodeproj/project.pbxproj | 16 +-- .../WhatsNew/Presentation/WhatsNewView.swift | 23 ++-- .../Presentation/WhatsNewViewModel.swift | 9 +- 112 files changed, 816 insertions(+), 715 deletions(-) diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index 8473bbbf8..1090a7dd4 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -708,7 +708,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -819,7 +819,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1043,7 +1043,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1136,7 +1136,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.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 = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.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 = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1483,7 +1483,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1518,7 +1518,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 4bf20dda2..4f9b3498b 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -11,15 +11,11 @@ import OEXFoundation import Theme public struct SignUpView: View { - - @State - private var disclosureGroupOpen: Bool = false - + @Environment(\.isHorizontal) private var isHorizontal - - @ObservedObject - private var viewModel: SignUpViewModel - + + @Bindable private var viewModel: SignUpViewModel + public init(viewModel: SignUpViewModel) { self.viewModel = viewModel Task { @@ -115,7 +111,7 @@ public struct SignUpView: View { ) if !viewModel.isShowProgress { - DisclosureGroup(isExpanded: $disclosureGroupOpen) { + DisclosureGroup(isExpanded: $viewModel.disclosureGroupOpen) { FieldsView( fields: optionalFields, router: viewModel.router, @@ -125,7 +121,7 @@ public struct SignUpView: View { ) .padding(.horizontal, 1) } label: { - Text(disclosureGroupOpen + Text(viewModel.disclosureGroupOpen ? AuthLocalization.SignUp.hideFields : AuthLocalization.SignUp.showFields) .font(Theme.Fonts.labelLarge) diff --git a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift index 88af34779..f22c157b7 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift @@ -15,14 +15,24 @@ import GoogleSignIn import MSAL @MainActor -public final class SignUpViewModel: ObservableObject { - - @Published var isShowProgress = false - @Published var scrollTo: Int? - @Published var showError: Bool = false - @Published var thirdPartyAuthSuccess: Bool = false +@Observable public final class SignUpViewModel { + + private let interactor: AuthInteractorProtocol + private let analytics: AuthorizationAnalytics + private let validator: Validator + + let router: AuthorizationRouter + let config: ConfigProtocol + let cssInjector: CSSInjector let sourceScreen: LogistrationSourceScreen - + let storage: CoreStorage + + var isShowProgress = false + var scrollTo: Int? + var showError: Bool = false + var thirdPartyAuthSuccess: Bool = false + var disclosureGroupOpen = false + var errorMessage: String? { didSet { withAnimation { @@ -31,7 +41,7 @@ public final class SignUpViewModel: ObservableObject { } } - @Published var fields: [FieldConfiguration] = [] + var fields: [FieldConfiguration] = [] var requiredFields: [FieldConfiguration] { fields.filter { $0.field.required && @@ -49,15 +59,7 @@ public final class SignUpViewModel: ObservableObject { fields.filter { !$0.field.required } } - let router: AuthorizationRouter - let config: ConfigProtocol - let cssInjector: CSSInjector - - private let interactor: AuthInteractorProtocol - private let analytics: AuthorizationAnalytics - private let validator: Validator var authMethod: AuthMethod = .password - let storage: CoreStorage public init( interactor: AuthInteractorProtocol, diff --git a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift index e069b75e1..8775d21d3 100644 --- a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift +++ b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift @@ -12,15 +12,10 @@ import Theme public struct ResetPasswordView: View { - @State private var email: String = "" - - @State private var isRecovered: Bool = false - @Environment(\.isHorizontal) private var isHorizontal - - @ObservedObject - private var viewModel: ResetPasswordViewModel - + + @Bindable private var viewModel: ResetPasswordViewModel + public init(viewModel: ResetPasswordViewModel) { self.viewModel = viewModel } @@ -46,7 +41,7 @@ public struct ResetPasswordView: View { ScrollView { VStack { - if isRecovered { + if viewModel.isRecovered { ZStack { VStack { CoreAssets.checkEmail.swiftUIImage @@ -62,7 +57,7 @@ public struct ResetPasswordView: View { .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 4) .accessibilityIdentifier("recover_title_text") - Text(AuthLocalization.Forgot.checkDescription + email) + Text(AuthLocalization.Forgot.checkDescription + viewModel.email) .font(Theme.Fonts.bodyMedium) .multilineTextAlignment(.center) .foregroundColor(Theme.Colors.textPrimary) @@ -93,7 +88,7 @@ public struct ResetPasswordView: View { .font(Theme.Fonts.labelLarge) .foregroundColor(Theme.Colors.textPrimary) .accessibilityIdentifier("email_text") - TextField("", text: $email) + TextField("", text: $viewModel.email) .font(Theme.Fonts.bodyLarge) .foregroundColor(Theme.Colors.textInputTextColor) .keyboardType(.emailAddress) @@ -104,7 +99,7 @@ public struct ResetPasswordView: View { .background( Theme.InputFieldBackground( placeHolder: AuthLocalization.SignIn.email, - text: email, + text: viewModel.email, padding: 15 ) ) @@ -123,7 +118,8 @@ public struct ResetPasswordView: View { } else { StyledButton(AuthLocalization.Forgot.request) { Task { - await viewModel.resetPassword(email: email, isRecovered: $isRecovered) + await viewModel.resetPassword(email: viewModel.email, + isRecovered: $viewModel.isRecovered) } } .padding(.top, 30) diff --git a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift index c58c593e4..0b5ddf040 100644 --- a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift +++ b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift @@ -10,11 +10,15 @@ import Core import OEXFoundation @MainActor -public final class ResetPasswordViewModel: ObservableObject { - - @Published private(set) var isShowProgress = false - @Published private(set) var showError: Bool = false - @Published private(set) var showAlert: Bool = false +@Observable public final class ResetPasswordViewModel { + + private(set) var isShowProgress = false + private(set) var showError: Bool = false + private(set) var showAlert: Bool = false + + var email: String = "" + var isRecovered: Bool = false + var errorMessage: String? { didSet { withAnimation { diff --git a/Authorization/Authorization/Presentation/SSO/SSOWebViewModel.swift b/Authorization/Authorization/Presentation/SSO/SSOWebViewModel.swift index a1bd57936..07ab806e2 100644 --- a/Authorization/Authorization/Presentation/SSO/SSOWebViewModel.swift +++ b/Authorization/Authorization/Presentation/SSO/SSOWebViewModel.swift @@ -16,11 +16,12 @@ import GoogleSignIn import MSAL @MainActor -public class SSOWebViewModel: ObservableObject { +@Observable +public class SSOWebViewModel { - @Published private(set) var isShowProgress = false - @Published private(set) var showError: Bool = false - @Published private(set) var showAlert: Bool = false + private(set) var isShowProgress = false + private(set) var showError: Bool = false + private(set) var showAlert: Bool = false let sourceScreen: LogistrationSourceScreen = .default var errorMessage: String? { diff --git a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift index 6fda03cb4..857c44c9e 100644 --- a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift +++ b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift @@ -12,13 +12,13 @@ import Theme struct SocialAuthView: View { // MARK: - Properties - @StateObject var viewModel: SocialAuthViewModel + var viewModel: SocialAuthViewModel init( authType: SocialAuthType = .signIn, viewModel: SocialAuthViewModel ) { - self._viewModel = .init(wrappedValue: viewModel) + self.viewModel = viewModel self.authType = authType } diff --git a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift index 8ebd6e35e..3050c7d81 100644 --- a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift +++ b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift @@ -57,14 +57,15 @@ enum SocialAuthDetails { } @MainActor -final public class SocialAuthViewModel: ObservableObject { +@Observable +final public class SocialAuthViewModel { // MARK: - Properties private var completion: ((Result) -> Void) private let config: ConfigProtocol - @Published var lastUsedOption: SocialAuthMethod? + var lastUsedOption: SocialAuthMethod? var enabledOptions: [SocialAuthMethod] = [] init( @@ -81,10 +82,10 @@ final public class SocialAuthViewModel: ObservableObject { configureEnabledOptions() } - private lazy var appleAuthProvider: AppleAuthProvider = .init(config: config) - private lazy var googleAuthProvider: GoogleAuthProvider = .init() - private lazy var facebookAuthProvider: FacebookAuthProvider = .init() - private lazy var microsoftAuthProvider: MicrosoftAuthProvider = .init() + @ObservationIgnored private lazy var appleAuthProvider: AppleAuthProvider = .init(config: config) + @ObservationIgnored private lazy var googleAuthProvider: GoogleAuthProvider = .init() + @ObservationIgnored private lazy var facebookAuthProvider: FacebookAuthProvider = .init() + @ObservationIgnored private lazy var microsoftAuthProvider: MicrosoftAuthProvider = .init() private var topViewController: UIViewController? { UIApplication.topViewController() diff --git a/Core/Core/AvoidingHelpers/State/KeyboardState.swift b/Core/Core/AvoidingHelpers/State/KeyboardState.swift index 005ada41a..338b90283 100644 --- a/Core/Core/AvoidingHelpers/State/KeyboardState.swift +++ b/Core/Core/AvoidingHelpers/State/KeyboardState.swift @@ -3,7 +3,7 @@ import SwiftUI import UIKit -public struct KeyboardState: Sendable { +public struct KeyboardState: Sendable, Equatable { public let animationDuration: TimeInterval /// Keyboard notification return a private curve value - 7. @@ -25,7 +25,6 @@ public struct KeyboardState: Sendable { // MARK: - Static -@MainActor extension KeyboardState { static let `default` = KeyboardState( animationDuration: 0, diff --git a/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift b/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift index 8a1f094a4..fa58b96be 100644 --- a/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift +++ b/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift @@ -1,17 +1,67 @@ // -import Combine +import SwiftUI @MainActor -final class KeyboardStateObserver: ObservableObject { - @Published private(set) var keyboardState: KeyboardState = .default +@Observable +final class KeyboardStateObserver { + private(set) var keyboardState: KeyboardState = .default - private var subscription: AnyCancellable? + private var observers: [NSObjectProtocol] = [] init() { - subscription = Publishers.keyboardStatePublisher - .sink(receiveValue: { [weak self] state in - self?.keyboardState = state - }) + let notificationCenter = NotificationCenter.default + + // Observe keyboard will show + let showObserver = notificationCenter.addObserver( + forName: UIResponder.keyboardWillShowNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let self = self else { return } + + if let state = KeyboardState.from(notification: notification) { + self.updateState(state) + } + } + + // Observe keyboard will change frame + let changeObserver = notificationCenter.addObserver( + forName: UIResponder.keyboardWillChangeFrameNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let self = self else { return } + // Extract data from notification synchronously on main queue + if let state = KeyboardState.from(notification: notification) { + self.updateState(state) + } + } + + // Observe keyboard will hide + let hideObserver = notificationCenter.addObserver( + forName: UIResponder.keyboardWillHideNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let self = self else { return } + // Extract data from notification synchronously on main queue + if let state = KeyboardState.from(notification: notification) { + self.updateState(state) + } + } + + observers = [showObserver, changeObserver, hideObserver] + } + + private func updateState(_ newState: KeyboardState) { + // Remove duplicates (same as removeDuplicates in Combine) + guard newState.height != keyboardState.height else { return } + keyboardState = newState + } + + @MainActor + deinit { + observers.forEach { NotificationCenter.default.removeObserver($0) } } } diff --git a/Core/Core/AvoidingHelpers/ViewModifiers/KeyboardAvoidingModifier.swift b/Core/Core/AvoidingHelpers/ViewModifiers/KeyboardAvoidingModifier.swift index 4a4879bb3..85f0a04e8 100644 --- a/Core/Core/AvoidingHelpers/ViewModifiers/KeyboardAvoidingModifier.swift +++ b/Core/Core/AvoidingHelpers/ViewModifiers/KeyboardAvoidingModifier.swift @@ -1,13 +1,14 @@ // -import Combine import SwiftUI -public class KeyboardScrollInvocator: ObservableObject { - var triggerSubject = PassthroughSubject() - +@MainActor +@Observable +public class KeyboardScrollInvocator { + var onTrigger: (() -> Void)? + public func scrollToActiveInput() { - triggerSubject.send(true) + onTrigger?() } } @@ -16,9 +17,9 @@ private struct KeyboardAvoidingModifier: ViewModifier { private let partialAvoidingPadding: CGFloat private let dismissKeyboardByTap: Bool private let onProvideScrollInvocator: ((KeyboardScrollInvocator) -> Void)? - - @StateObject private var keyboardObserver = KeyboardStateObserver() - @StateObject private var scrollInvocator = KeyboardScrollInvocator() + + private var keyboardObserver = KeyboardStateObserver() + @State private var scrollInvocator = KeyboardScrollInvocator() init( scrollerOptions: KeyboardScrollerOptions?, @@ -38,19 +39,19 @@ private struct KeyboardAvoidingModifier: ViewModifier { .ignoresSafeArea(.keyboard, edges: .bottom) } .ignoresSafeArea(.keyboard, edges: .bottom) - + // for fields - .onReceive(keyboardObserver.$keyboardState.receive(on: DispatchQueue.main)) { state in + .onChange(of: keyboardObserver.keyboardState) { _, state in if state.height == 0 { DismissKeyboardTapHandler.shared.isEnabled = false return } - + // Applied to the whole UIWindow. Use addTapToEndEditing() modifier to apply locally. if dismissKeyboardByTap { DismissKeyboardTapHandler.shared.isEnabled = true } - + if let options = scrollerOptions { KeyboardScroller.scroll( keyboardState: state, @@ -59,19 +60,20 @@ private struct KeyboardAvoidingModifier: ViewModifier { ) } } - .onReceive(scrollInvocator.triggerSubject) { _ in - guard !keyboardObserver.keyboardState.height.isZero, - let options = scrollerOptions else { - return - } - - KeyboardScroller.scroll( - keyboardState: keyboardObserver.keyboardState, - options: options, - partialAvoidingPadding: partialAvoidingPadding - ) - } .onAppear { + // Setup callback for manual scroll triggering + scrollInvocator.onTrigger = { [keyboardObserver, scrollerOptions, partialAvoidingPadding] in + guard !keyboardObserver.keyboardState.height.isZero, + let options = scrollerOptions else { + return + } + + KeyboardScroller.scroll( + keyboardState: keyboardObserver.keyboardState, + options: options, + partialAvoidingPadding: partialAvoidingPadding + ) + } onProvideScrollInvocator?(scrollInvocator) } } diff --git a/Core/Core/View/Base/AppReview/AppReviewView.swift b/Core/Core/View/Base/AppReview/AppReviewView.swift index efe5df5bd..550e0a00f 100644 --- a/Core/Core/View/Base/AppReview/AppReviewView.swift +++ b/Core/Core/View/Base/AppReview/AppReviewView.swift @@ -11,7 +11,7 @@ import Theme public struct AppReviewView: View { - @ObservedObject private var viewModel: AppReviewViewModel + @Bindable private var viewModel: AppReviewViewModel @Environment(\.isHorizontal) private var isHorizontal @Environment(\.presentationMode) private var presentationMode diff --git a/Core/Core/View/Base/AppReview/AppReviewViewModel.swift b/Core/Core/View/Base/AppReview/AppReviewViewModel.swift index 4b9f66c64..7de5cc800 100644 --- a/Core/Core/View/Base/AppReview/AppReviewViewModel.swift +++ b/Core/Core/View/Base/AppReview/AppReviewViewModel.swift @@ -9,7 +9,8 @@ import SwiftUI import StoreKit @MainActor -public class AppReviewViewModel: ObservableObject { +@Observable +public class AppReviewViewModel { enum ReviewState { case vote @@ -42,12 +43,12 @@ public class AppReviewViewModel: ObservableObject { } } - @Published var state: ReviewState = .vote - @Published var rating: Int = 0 - @Published var showReview: Bool = false - @Published var showSelectMailClientView: Bool = false - @Published var feedback: String = "" - @Published var clients: [ThirdPartyMailClient] = [] + var state: ReviewState = .vote + var rating: Int = 0 + var showReview: Bool = false + var showSelectMailClientView: Bool = false + var feedback: String = "" + var clients: [ThirdPartyMailClient] = [] let allClients = ThirdPartyMailClient.clients private let config: ConfigProtocol diff --git a/Core/Core/View/Base/BackNavigationButton.swift b/Core/Core/View/Base/BackNavigationButton.swift index 415433cd1..d88782b90 100644 --- a/Core/Core/View/Base/BackNavigationButton.swift +++ b/Core/Core/View/Base/BackNavigationButton.swift @@ -15,7 +15,7 @@ class BackButton: UIButton { } public struct BackNavigationButtonRepresentable: UIViewRepresentable { - @ObservedObject var viewModel: BackNavigationButtonViewModel + var viewModel: BackNavigationButtonViewModel var action: (() -> Void)? var color: Color @@ -64,7 +64,7 @@ public struct BackNavigationButtonRepresentable: UIViewRepresentable { } public struct BackNavigationButton: View { - @StateObject var viewModel = BackNavigationButtonViewModel() + var viewModel = BackNavigationButtonViewModel() private let color: Color private let action: (() -> Void)? diff --git a/Core/Core/View/Base/BackNavigationButtonViewModel.swift b/Core/Core/View/Base/BackNavigationButtonViewModel.swift index cce158e2c..9993bc784 100644 --- a/Core/Core/View/Base/BackNavigationButtonViewModel.swift +++ b/Core/Core/View/Base/BackNavigationButtonViewModel.swift @@ -26,9 +26,10 @@ public struct BackNavigationMenuItem: Identifiable { } @MainActor -class BackNavigationButtonViewModel: ObservableObject { +@Observable +class BackNavigationButtonViewModel { private let helper: BackNavigationProtocol - @Published var items: [BackNavigationMenuItem] = [] + var items: [BackNavigationMenuItem] = [] init() { self.helper = Container.shared.resolve(BackNavigationProtocol.self)! diff --git a/Core/Core/View/Base/FieldConfiguration.swift b/Core/Core/View/Base/FieldConfiguration.swift index 63ce055e7..175bc8219 100644 --- a/Core/Core/View/Base/FieldConfiguration.swift +++ b/Core/Core/View/Base/FieldConfiguration.swift @@ -8,23 +8,24 @@ import Foundation import SwiftUI -public class FieldConfiguration: ObservableObject { - @Published public var shake: Bool = false - @Published public var error: String { +@Observable +public class FieldConfiguration { + public var shake: Bool = false + public var error: String { didSet { if error.count > 0 { shake = true } } } - @Published public var text: String { + public var text: String { didSet { error = "" shake = false } } - @Published public var selectedItem: PickerItem? + public var selectedItem: PickerItem? public let field: PickerFields public init(error: String = "", text: String = "", field: PickerFields, selectedItem: PickerItem? = nil) { diff --git a/Core/Core/View/Base/FileWebView.swift b/Core/Core/View/Base/FileWebView.swift index b6b98f4e5..3537fd789 100644 --- a/Core/Core/View/Base/FileWebView.swift +++ b/Core/Core/View/Base/FileWebView.swift @@ -47,17 +47,18 @@ public struct FileWebView: UIViewRepresentable { public func updateUIView(_ webview: WKWebView, context: Context) { } - - public class ViewModel: ObservableObject { + + @Observable + public class ViewModel { - @Published var url: String + var url: String public init(url: String) { self.url = url } } - @ObservedObject var viewModel: ViewModel + var viewModel: ViewModel public init(viewModel: ViewModel) { self.viewModel = viewModel diff --git a/Core/Core/View/Base/PickerView.swift b/Core/Core/View/Base/PickerView.swift index e0403b822..9cac4f0e4 100644 --- a/Core/Core/View/Base/PickerView.swift +++ b/Core/Core/View/Base/PickerView.swift @@ -9,9 +9,8 @@ import SwiftUI import Theme public struct PickerView: View { - - @ObservedObject - private var config: FieldConfiguration + + @Bindable private var config: FieldConfiguration private var router: BaseRouter public init(config: FieldConfiguration, router: BaseRouter) { diff --git a/Core/Core/View/Base/RegistrationTextField.swift b/Core/Core/View/Base/RegistrationTextField.swift index 83cb3eb24..ffec300fd 100644 --- a/Core/Core/View/Base/RegistrationTextField.swift +++ b/Core/Core/View/Base/RegistrationTextField.swift @@ -10,16 +10,14 @@ import Theme public struct RegistrationTextField: View { - @State public var shakeIt: Bool = false @State public var placeholder: String = "" public var keyboardType: UIKeyboardType public var textContentType: UITextContentType private var isTextArea: Bool private var scrollTo: (() -> Void) = {} - @ObservedObject - private var config: FieldConfiguration - + @Bindable private var config: FieldConfiguration + public init(config: FieldConfiguration, isTextArea: Bool = false, keyboardType: UIKeyboardType = .default, diff --git a/Core/Core/View/Base/VideoDownloadQualityView.swift b/Core/Core/View/Base/VideoDownloadQualityView.swift index 2fd29240a..576e5d9da 100644 --- a/Core/Core/View/Base/VideoDownloadQualityView.swift +++ b/Core/Core/View/Base/VideoDownloadQualityView.swift @@ -9,12 +9,13 @@ import SwiftUI import Kingfisher import Theme -public final class VideoDownloadQualityViewModel: ObservableObject { +@Observable +public final class VideoDownloadQualityViewModel { var didSelect: ((DownloadQuality) -> Void)? let downloadQuality = DownloadQuality.allCases - @Published var selectedDownloadQuality: DownloadQuality { + var selectedDownloadQuality: DownloadQuality { willSet { if newValue != selectedDownloadQuality { didSelect?(newValue) @@ -30,7 +31,6 @@ public final class VideoDownloadQualityViewModel: ObservableObject { public struct VideoDownloadQualityView: View { - @StateObject private var viewModel: VideoDownloadQualityViewModel private var analytics: CoreAnalytics private var router: BaseRouter @@ -44,11 +44,9 @@ public struct VideoDownloadQualityView: View { router: BaseRouter, isModal: Bool = false ) { - self._viewModel = StateObject( - wrappedValue: .init( - downloadQuality: downloadQuality, - didSelect: didSelect - ) + self.viewModel = VideoDownloadQualityViewModel( + downloadQuality: downloadQuality, + didSelect: didSelect ) self.analytics = analytics self.router = router diff --git a/Core/Core/View/Base/WebUnitView.swift b/Core/Core/View/Base/WebUnitView.swift index bb143560f..e3a0a9eab 100644 --- a/Core/Core/View/Base/WebUnitView.swift +++ b/Core/Core/View/Base/WebUnitView.swift @@ -11,7 +11,7 @@ import Theme public struct WebUnitView: View { - @StateObject private var viewModel: WebUnitViewModel + private var viewModel: WebUnitViewModel @State private var isWebViewLoading = false private var url: String @@ -30,9 +30,7 @@ public struct WebUnitView: View { injections: [WebviewInjection]?, blockID: String ) { - self._viewModel = .init( - wrappedValue: viewModel - ) + self.viewModel = viewModel self.url = url self.dataUrl = dataUrl self.connectivity = connectivity diff --git a/Core/Core/View/Base/WebUnitViewModel.swift b/Core/Core/View/Base/WebUnitViewModel.swift index 693a2e4b6..526dfadd9 100644 --- a/Core/Core/View/Base/WebUnitViewModel.swift +++ b/Core/Core/View/Base/WebUnitViewModel.swift @@ -8,15 +8,16 @@ import Foundation import SwiftUI -public final class WebUnitViewModel: ObservableObject, WebviewCookiesUpdateProtocol { +@Observable +public final class WebUnitViewModel: WebviewCookiesUpdateProtocol { public let authInteractor: AuthInteractorProtocol let config: ConfigProtocol let syncManager: OfflineSyncManagerProtocol - @Published public var updatingCookies: Bool = false - @Published public var cookiesReady: Bool = false - @Published public var showError: Bool = false + public var updatingCookies: Bool = false + public var cookiesReady: Bool = false + public var showError: Bool = false private var retryCount = 1 public var errorMessage: String? { diff --git a/Core/Core/View/Base/Webview/WebView.swift b/Core/Core/View/Base/Webview/WebView.swift index bfd43a9f9..2dc0e737d 100644 --- a/Core/Core/View/Base/Webview/WebView.swift +++ b/Core/Core/View/Base/Webview/WebView.swift @@ -24,9 +24,10 @@ public protocol WebViewNavigationDelegate: AnyObject { public struct WebView: UIViewRepresentable { - public class ViewModel: ObservableObject { - - @Published var url: String + @Observable + public class ViewModel { + + var url: String let baseURL: String let injections: [WebviewInjection]? var openFile: (String) -> Void @@ -44,7 +45,7 @@ public struct WebView: UIViewRepresentable { } } - @ObservedObject var viewModel: ViewModel + var viewModel: ViewModel @Binding public var isLoading: Bool var webViewNavDelegate: WebViewNavigationDelegate? let connectivity: ConnectivityProtocol diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index a2c9717ef..9ec7cfb3b 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -94,10 +94,10 @@ A52897DC2E276B12009C7044 /* CourseGradeCarouselSlideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52897DB2E276B02009C7044 /* CourseGradeCarouselSlideView.swift */; }; A52897DE2E277A7C009C7044 /* GradeItemCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52897DD2E277A7C009C7044 /* GradeItemCarouselView.swift */; }; A52897E02E278D7D009C7044 /* ViewAllButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A52897DF2E278D7D009C7044 /* ViewAllButton.swift */; }; + A59016D52E69B2E900076220 /* VideoNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59016D42E69B2E900076220 /* VideoNavigationView.swift */; }; A5A510A22E66E8D600553607 /* CourseVideoCarouselSlideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A510A12E66E8D600553607 /* CourseVideoCarouselSlideView.swift */; }; A5A510A42E6728AD00553607 /* CourseAssignmentsCarouselSlideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A510A32E6728AD00553607 /* CourseAssignmentsCarouselSlideView.swift */; }; A5A510A62E672E4A00553607 /* AssignmentCarouselDetailCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A510A52E672E4A00553607 /* AssignmentCarouselDetailCardView.swift */; }; - A59016D52E69B2E900076220 /* VideoNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59016D42E69B2E900076220 /* VideoNavigationView.swift */; }; B8F50317B6B830A0E520C954 /* Pods_App_Course_CourseTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50E59D2B81E12610964282C5 /* Pods_App_Course_CourseTests.framework */; }; BA58CF5D2B3D804D005B102E /* CourseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF5C2B3D804D005B102E /* CourseStorage.swift */; }; BA58CF612B471041005B102E /* VideoDownloadQualityBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF602B471041005B102E /* VideoDownloadQualityBarView.swift */; }; @@ -279,10 +279,10 @@ A52897DB2E276B02009C7044 /* CourseGradeCarouselSlideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseGradeCarouselSlideView.swift; sourceTree = ""; }; A52897DD2E277A7C009C7044 /* GradeItemCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradeItemCarouselView.swift; sourceTree = ""; }; A52897DF2E278D7D009C7044 /* ViewAllButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewAllButton.swift; sourceTree = ""; }; + A59016D42E69B2E900076220 /* VideoNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoNavigationView.swift; sourceTree = ""; }; A5A510A12E66E8D600553607 /* CourseVideoCarouselSlideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVideoCarouselSlideView.swift; sourceTree = ""; }; A5A510A32E6728AD00553607 /* CourseAssignmentsCarouselSlideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseAssignmentsCarouselSlideView.swift; sourceTree = ""; }; A5A510A52E672E4A00553607 /* AssignmentCarouselDetailCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignmentCarouselDetailCardView.swift; sourceTree = ""; }; - A59016D42E69B2E900076220 /* VideoNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoNavigationView.swift; sourceTree = ""; }; ADC2A1B8183A674705F5F7E2 /* Pods-App-Course.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course.debug.xcconfig"; path = "Target Support Files/Pods-App-Course/Pods-App-Course.debug.xcconfig"; sourceTree = ""; }; B196A14555D0E006995A5683 /* Pods-App-CourseDetails.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-CourseDetails.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-CourseDetails/Pods-App-CourseDetails.releaseprod.xcconfig"; sourceTree = ""; }; BA58CF5C2B3D804D005B102E /* CourseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStorage.swift; sourceTree = ""; }; @@ -1039,10 +1039,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests-resources.sh\"\n"; @@ -1544,7 +1548,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1583,7 +1587,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1685,7 +1689,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1788,7 +1792,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1885,7 +1889,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1981,7 +1985,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2083,7 +2087,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2202,7 +2206,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 3a5bb5335..43f28ebfa 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -15,11 +15,11 @@ import Theme public struct CourseContainerView: View { @Bindable public var viewModel: CourseContainerViewModel - @ObservedObject public var courseDatesViewModel: CourseDatesViewModel - @ObservedObject public var courseProgressViewModel: CourseProgressViewModel + @State private var isAnimatingForTap: Bool = false + @State private var discussionTopicsViewModel: DiscussionTopicsViewModel public var courseID: String private var title: String @State private var ignoreOffset: Bool = false @@ -30,6 +30,7 @@ public struct CourseContainerView: View { @Environment(\.isHorizontal) private var isHorizontal @Namespace private var animationNamespace private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + private let discussionRouter: DiscussionRouter private let coordinateBoundaryLower: CGFloat = -115 private let courseRawImage: String? @@ -59,6 +60,12 @@ public struct CourseContainerView: View { title: String, courseRawImage: String? ) { + let resolvedDiscussionTopicsViewModel = Container.shared.resolve( + DiscussionTopicsViewModel.self, + argument: title + )! + let resolvedDiscussionRouter = Container.shared.resolve(DiscussionRouter.self)! + self._discussionTopicsViewModel = State(initialValue: resolvedDiscussionTopicsViewModel) self.viewModel = viewModel self.courseDatesViewModel = courseDatesViewModel self.courseProgressViewModel = courseProgressViewModel @@ -75,6 +82,7 @@ public struct CourseContainerView: View { self.courseID = courseID self.title = title self.courseRawImage = courseRawImage + self.discussionRouter = resolvedDiscussionRouter } public var body: some View { @@ -280,9 +288,8 @@ public struct CourseContainerView: View { coordinate: $coordinate, collapsed: $collapsed, viewHeight: $viewHeight, - viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self, - argument: title)!, - router: Container.shared.resolve(DiscussionRouter.self)! + viewModel: discussionTopicsViewModel, + router: discussionRouter ) .tabItem { tab.image diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift index 5de224b44..d42a939e5 100644 --- a/Course/Course/Presentation/Dates/CourseDatesView.swift +++ b/Course/Course/Presentation/Dates/CourseDatesView.swift @@ -15,8 +15,7 @@ import SwiftUIIntrospect public struct CourseDatesView: View { private let courseID: String - - @StateObject + private var viewModel: CourseDatesViewModel @Binding private var coordinate: CGFloat @Binding private var collapsed: Bool @@ -33,7 +32,7 @@ public struct CourseDatesView: View { self._coordinate = coordinate self._collapsed = collapsed self._viewHeight = viewHeight - self._viewModel = StateObject(wrappedValue: viewModel) + self.viewModel = viewModel } public var body: some View { diff --git a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift index 8ab0d022e..2fe9188f5 100644 --- a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift +++ b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift @@ -11,7 +11,8 @@ import SwiftUI import OEXFoundation @MainActor -public class CourseDatesViewModel: ObservableObject { +@Observable +public class CourseDatesViewModel { enum EventState: Sendable { case addedCalendar @@ -21,11 +22,11 @@ public class CourseDatesViewModel: ObservableObject { case none } - @Published var isShowProgress = true - @Published var showError: Bool = false - @Published var courseDates: CourseDates? - @Published var isOn: Bool = false - @Published var eventState: EventState? + var isShowProgress = true + var showError: Bool = false + var courseDates: CourseDates? + var isOn: Bool = false + var eventState: EventState? var errorMessage: String? { didSet { diff --git a/Course/Course/Presentation/Dates/Elements/CourseDateListView.swift b/Course/Course/Presentation/Dates/Elements/CourseDateListView.swift index d31c9a832..bb374f73d 100644 --- a/Course/Course/Presentation/Dates/Elements/CourseDateListView.swift +++ b/Course/Course/Presentation/Dates/Elements/CourseDateListView.swift @@ -10,7 +10,9 @@ import Core import Theme struct CourseDateListView: View { - @ObservedObject var viewModel: CourseDatesViewModel + + var viewModel: CourseDatesViewModel + @State private var isExpanded = false @Binding var coordinate: CGFloat @Binding var collapsed: Bool diff --git a/Course/Course/Presentation/Downloads/DownloadsView.swift b/Course/Course/Presentation/Downloads/DownloadsView.swift index 61ae87026..c9e0ab507 100644 --- a/Course/Course/Presentation/Downloads/DownloadsView.swift +++ b/Course/Course/Presentation/Downloads/DownloadsView.swift @@ -16,7 +16,7 @@ public struct DownloadsView: View { @Environment(\.dismiss) private var dismiss @Environment(\.isHorizontal) private var isHorizontal - @StateObject private var viewModel: DownloadsViewModel + private var viewModel: DownloadsViewModel var isSheet: Bool = true @@ -26,12 +26,11 @@ public struct DownloadsView: View { courseHelper: CourseDownloadHelperProtocol ) { self.isSheet = isSheet - self._viewModel = .init( - wrappedValue: .init( - router: router, - helper: courseHelper - ) + self.viewModel = DownloadsViewModel( + router: router, + helper: courseHelper ) + } // MARK: - Body diff --git a/Course/Course/Presentation/Downloads/DownloadsViewModel.swift b/Course/Course/Presentation/Downloads/DownloadsViewModel.swift index 3b902d644..92b5f6c5f 100644 --- a/Course/Course/Presentation/Downloads/DownloadsViewModel.swift +++ b/Course/Course/Presentation/Downloads/DownloadsViewModel.swift @@ -11,11 +11,12 @@ import OEXFoundation @preconcurrency import Combine @MainActor -final class DownloadsViewModel: ObservableObject { +@Observable +final class DownloadsViewModel { // MARK: - Properties - @Published private(set) var downloads: [DownloadDataTask] = [] + private(set) var downloads: [DownloadDataTask] = [] let router: CourseRouter diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index 82d74a408..e69d57ef6 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -15,8 +15,7 @@ struct HandoutsView: View { @Binding private var coordinate: CGFloat @Binding private var collapsed: Bool @Binding private var viewHeight: CGFloat - - @StateObject + private var viewModel: HandoutsViewModel public init( @@ -30,7 +29,7 @@ struct HandoutsView: View { self._coordinate = coordinate self._collapsed = collapsed self._viewHeight = viewHeight - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel } public var body: some View { diff --git a/Course/Course/Presentation/Handouts/HandoutsViewModel.swift b/Course/Course/Presentation/Handouts/HandoutsViewModel.swift index c1756d9ed..16467ba35 100644 --- a/Course/Course/Presentation/Handouts/HandoutsViewModel.swift +++ b/Course/Course/Presentation/Handouts/HandoutsViewModel.swift @@ -10,12 +10,13 @@ import Core import SwiftUI @MainActor -public final class HandoutsViewModel: ObservableObject { +@Observable +public final class HandoutsViewModel { - @Published private(set) var isShowProgress = false - @Published var showError: Bool = false - @Published var handouts: String? - @Published var updates: [CourseUpdate] = [] + private(set) var isShowProgress = false + var showError: Bool = false + var handouts: String? + var updates: [CourseUpdate] = [] var errorMessage: String? { didSet { diff --git a/Course/Course/Presentation/NewOutlIineAndProgress/ CourseOutlineAndProgressViewModel.swift b/Course/Course/Presentation/NewOutlIineAndProgress/ CourseOutlineAndProgressViewModel.swift index e2da524af..fe2ba7b6b 100644 --- a/Course/Course/Presentation/NewOutlIineAndProgress/ CourseOutlineAndProgressViewModel.swift +++ b/Course/Course/Presentation/NewOutlIineAndProgress/ CourseOutlineAndProgressViewModel.swift @@ -6,14 +6,15 @@ import OEXFoundation import Combine @MainActor -public class CourseOutlineAndProgressViewModel: ObservableObject { +@Observable +public class CourseOutlineAndProgressViewModel { // MARK: - Variables - @Published public var courseProgress: CourseProgressDetails? - @Published public var showError: Bool = false - @Published public var selection: Int - @Published var userSettings: UserSettings? - @Published var isInternetAvaliable: Bool = true + public var courseProgress: CourseProgressDetails? + public var showError: Bool = false + public var selection: Int + var userSettings: UserSettings? + var isInternetAvaliable: Bool = true let router: CourseRouter let analytics: CourseAnalytics diff --git a/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift b/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift index 57e7d623c..a2416dac9 100644 --- a/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift +++ b/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift @@ -10,7 +10,7 @@ public struct CourseOutlineAndProgressView: View { // MARK: - Variables @Bindable private var viewModelContainer: CourseContainerViewModel - @StateObject private var viewModelProgress: CourseProgressViewModel + private var viewModelProgress: CourseProgressViewModel private let title: String private let courseID: String private let isVideo: Bool @@ -94,7 +94,7 @@ public struct CourseOutlineAndProgressView: View { ) { self.title = title self.viewModelContainer = viewModelContainer - self._viewModelProgress = StateObject(wrappedValue: { viewModelProgress}()) + self.viewModelProgress = viewModelProgress self.courseID = courseID self.isVideo = isVideo self._selection = selection diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift index 88a0efc0b..18db60009 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift @@ -16,7 +16,7 @@ public struct CourseVerticalView: View { private var title: String private var courseName: String private var courseID: String - @ObservedObject + private var viewModel: CourseVerticalViewModel private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift index 98df0cc27..036be081c 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift @@ -9,12 +9,13 @@ import SwiftUI import Core import OEXFoundation -public final class CourseVerticalViewModel: ObservableObject, @unchecked Sendable { +@Observable +public final class CourseVerticalViewModel: @unchecked Sendable { let router: CourseRouter let analytics: CourseAnalytics let connectivity: ConnectivityProtocol - @Published var verticals: [CourseVertical] - @Published var showError: Bool = false + var verticals: [CourseVertical] + var showError: Bool = false let chapters: [CourseChapter] let chapterIndex: Int let sequentialIndex: Int diff --git a/Course/Course/Presentation/Progress/CourseProgressScreenView.swift b/Course/Course/Presentation/Progress/CourseProgressScreenView.swift index 3ee35119e..d5ab79476 100644 --- a/Course/Course/Presentation/Progress/CourseProgressScreenView.swift +++ b/Course/Course/Presentation/Progress/CourseProgressScreenView.swift @@ -17,8 +17,8 @@ struct CourseProgressScreenView: View { @Binding private var collapsed: Bool @Binding private var viewHeight: CGFloat - @StateObject - private var viewModel: CourseProgressViewModel + @Bindable private var viewModel: CourseProgressViewModel + private let initialCourseStructure: CourseStructure? private let connectivity: ConnectivityProtocol @@ -36,7 +36,7 @@ struct CourseProgressScreenView: View { self._coordinate = coordinate self._collapsed = collapsed self._viewHeight = viewHeight - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.connectivity = connectivity self.initialCourseStructure = courseStructure } diff --git a/Course/Course/Presentation/Progress/CourseProgressViewModel.swift b/Course/Course/Presentation/Progress/CourseProgressViewModel.swift index 5f0068c52..a41bad855 100644 --- a/Course/Course/Presentation/Progress/CourseProgressViewModel.swift +++ b/Course/Course/Presentation/Progress/CourseProgressViewModel.swift @@ -11,13 +11,14 @@ import Foundation import Theme @MainActor -public class CourseProgressViewModel: ObservableObject { - - @Published var courseProgress: CourseProgressDetails? - @Published var assignmentProgressData: [String: AssignmentProgressData] = [:] - @Published var isLoading: Bool = false - @Published var isShowRefresh = false - @Published var showError: Bool = false +@Observable +public class CourseProgressViewModel { + + var courseProgress: CourseProgressDetails? + var assignmentProgressData: [String: AssignmentProgressData] = [:] + var isLoading: Bool = false + var isShowRefresh = false + var showError: Bool = false let router: CourseRouter let analytics: CourseAnalytics diff --git a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseAssignmentsCarouselSlideView.swift b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseAssignmentsCarouselSlideView.swift index d11fda7fa..d7c793909 100644 --- a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseAssignmentsCarouselSlideView.swift +++ b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseAssignmentsCarouselSlideView.swift @@ -5,7 +5,7 @@ import Core struct CourseAssignmentsCarouselSlideView: View { // MARK: - Variables - @ObservedObject var viewModelProgress: CourseProgressViewModel + var viewModelProgress: CourseProgressViewModel var viewModelContainer: CourseContainerViewModel @Environment(\.horizontalSizeClass) private var horizontalSizeClass diff --git a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseGradeCarouselSlideView.swift b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseGradeCarouselSlideView.swift index ea7feaa98..de1c4f48f 100644 --- a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseGradeCarouselSlideView.swift +++ b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseGradeCarouselSlideView.swift @@ -5,8 +5,8 @@ import Core struct CourseGradeCarouselSlideView: View { // MARK: - Variables - @ObservedObject var viewModelProgress: CourseProgressViewModel - var viewModelContainer: CourseContainerViewModel + @Bindable var viewModelProgress: CourseProgressViewModel + var viewModelContainer: CourseContainerViewModel // MARK: - Body var body: some View { diff --git a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift index 1fdae9155..0bdb40064 100644 --- a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift +++ b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift @@ -5,7 +5,7 @@ import Core struct CourseVideoCarouselSlideView: View { // MARK: - Variables - @ObservedObject var viewModelProgress: CourseProgressViewModel + var viewModelProgress: CourseProgressViewModel var viewModelContainer: CourseContainerViewModel @State private var isHidingCompletedSections = true diff --git a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift index ddc9fc94e..cd816465c 100644 --- a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift +++ b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift @@ -14,7 +14,7 @@ struct CourseVideoDownloadBarView: View { // MARK: - Properties - @StateObject var viewModel: CourseVideoDownloadBarViewModel + var viewModel: CourseVideoDownloadBarViewModel private var onTap: (() -> Void)? private var onNotInternetAvaliable: (() -> Void)? @@ -25,13 +25,12 @@ struct CourseVideoDownloadBarView: View { onTap: (() -> Void)? = nil, analytics: CourseAnalytics ) { - self._viewModel = .init( - wrappedValue: .init( - courseStructure: courseStructure, - courseViewModel: courseViewModel, - analytics: analytics - ) + self.viewModel = CourseVideoDownloadBarViewModel( + courseStructure: courseStructure, + courseViewModel: courseViewModel, + analytics: analytics ) + self.onNotInternetAvaliable = onNotInternetAvaliable self.onTap = onTap } diff --git a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift index ac8aa78bd..f4abfeaa2 100644 --- a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift +++ b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift @@ -11,7 +11,8 @@ import OEXFoundation import Combine @MainActor -final class CourseVideoDownloadBarViewModel: ObservableObject { +@Observable +final class CourseVideoDownloadBarViewModel { // MARK: - Properties @@ -19,8 +20,8 @@ final class CourseVideoDownloadBarViewModel: ObservableObject { private let courseViewModel: CourseContainerViewModel private let analytics: CourseAnalytics - @Published private(set) var currentDownloadTask: DownloadDataTask? - @Published private(set) var isOn: Bool = false + private(set) var currentDownloadTask: DownloadDataTask? + private(set) var isOn: Bool = false private var cancellables = Set() diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index 52e2428f1..7f8ece7e3 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -18,8 +18,7 @@ public enum VideoPlayerState: Sendable { public struct EncodedVideoPlayer: View { - @StateObject - private var viewModel: EncodedVideoPlayerViewModel + @Bindable private var viewModel: EncodedVideoPlayerViewModel private var isOnScreen: Bool @@ -45,7 +44,7 @@ public struct EncodedVideoPlayer: View { viewModel: EncodedVideoPlayerViewModel, isOnScreen: Bool ) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.isOnScreen = isOnScreen } @@ -119,7 +118,7 @@ public struct EncodedVideoPlayer: View { viewModel.controller.player?.allowsExternalPlayback = true viewModel.controller.setNeedsStatusBarAppearanceUpdate() } - .onReceive(viewModel.$currentTime) { currentTime in + .onChange(of: viewModel.currentTime) { _, currentTime in let subtitle = viewModel.findSubtitle(at: Date(milliseconds: currentTime)) subtitleText = subtitle?.text ?? "" } diff --git a/Course/Course/Presentation/Video/SubtitlesView.swift b/Course/Course/Presentation/Video/SubtitlesView.swift index 1680f2191..37e7e242f 100644 --- a/Course/Course/Presentation/Video/SubtitlesView.swift +++ b/Course/Course/Presentation/Video/SubtitlesView.swift @@ -19,7 +19,6 @@ public struct SubtitlesView: View { @Environment(\.isHorizontal) private var isHorizontal - @ObservedObject private var viewModel: VideoPlayerViewModel private var scrollTo: ((Date) -> Void) = { _ in } diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index ec718d570..3c4fafbbd 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -12,21 +12,22 @@ import _AVKit_SwiftUI import Combine @MainActor -public class VideoPlayerViewModel: ObservableObject { - @Published var pause: Bool = false - @Published var currentTime: Double = 0 - @Published var isLoading: Bool = true - @Published var isLocalProgressApplied: Bool = false +@Observable +public class VideoPlayerViewModel { + var pause: Bool = false + var currentTime: Double = 0 + var isLoading: Bool = true + var isLocalProgressApplied: Bool = false public let connectivity: ConnectivityProtocol private var subtitlesDownloaded: Bool = false - @Published var subtitles: [Subtitle] = [] + var subtitles: [Subtitle] = [] var languages: [SubtitleUrl] - @Published var items: [PickerItem] = [] - @Published var selectedLanguage: String? + var items: [PickerItem] = [] + var selectedLanguage: String? - @Published var showError: Bool = false + var showError: Bool = false var errorMessage: String? { didSet { showError = errorMessage != nil diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index cca23484f..e5c65ce57 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -13,8 +13,7 @@ import Swinject public struct YouTubeVideoPlayer: View { - @StateObject - private var viewModel: YouTubeVideoPlayerViewModel + @Bindable private var viewModel: YouTubeVideoPlayerViewModel private var isOnScreen: Bool @State private var showAlert = false @@ -30,7 +29,7 @@ public struct YouTubeVideoPlayer: View { @Environment(\.isHorizontal) private var isHorizontal public init(viewModel: YouTubeVideoPlayerViewModel, isOnScreen: Bool) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.isOnScreen = isOnScreen } diff --git a/Dashboard/Dashboard.xcodeproj/project.pbxproj b/Dashboard/Dashboard.xcodeproj/project.pbxproj index dc5f31b57..3bac1c6e6 100644 --- a/Dashboard/Dashboard.xcodeproj/project.pbxproj +++ b/Dashboard/Dashboard.xcodeproj/project.pbxproj @@ -760,7 +760,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -874,7 +874,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1051,7 +1051,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1086,7 +1086,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1184,7 +1184,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1277,7 +1277,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1375,7 +1375,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1468,7 +1468,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index 8b8e77acc..6f16449eb 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -13,8 +13,7 @@ import Theme @MainActor public struct AllCoursesView: View { - @ObservedObject - private var viewModel: AllCoursesViewModel + @Bindable private var viewModel: AllCoursesViewModel private let router: DashboardRouter @Environment(\.isHorizontal) private var isHorizontal private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } diff --git a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift index dda60e937..11aebd5ae 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift @@ -11,16 +11,17 @@ import SwiftUI import Combine @MainActor -public class AllCoursesViewModel: ObservableObject { +@Observable +public class AllCoursesViewModel { var nextPage = 1 var totalPages = 1 - @Published private(set) var fetchInProgress = false - @Published private(set) var refresh = false - @Published var selectedMenu: CategoryOption = .all + private(set) var fetchInProgress = false + private(set) var refresh = false + var selectedMenu: CategoryOption = .all - @Published var myEnrollments: PrimaryEnrollment? - @Published var showError: Bool = false + var myEnrollments: PrimaryEnrollment? + var showError: Bool = false var errorMessage: String? { didSet { withAnimation { diff --git a/Dashboard/Dashboard/Presentation/ListDashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift index ac397f6d6..7c7da861c 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -25,13 +25,12 @@ public struct ListDashboardView: View { .accessibilityElement(children: .ignore) .accessibilityLabel(DashboardLocalization.Header.courses + DashboardLocalization.Header.welcomeBack) - @StateObject - private var viewModel: ListDashboardViewModel + @Bindable private var viewModel: ListDashboardViewModel private let router: DashboardRouter private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } public init(viewModel: ListDashboardViewModel, router: DashboardRouter) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.router = router } diff --git a/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift index 8fc917b00..773859414 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift @@ -11,14 +11,15 @@ import SwiftUI import Combine @MainActor -public class ListDashboardViewModel: ObservableObject { +@Observable +public class ListDashboardViewModel { public var nextPage = 1 public var totalPages = 1 - @Published public private(set) var fetchInProgress = false + public private(set) var fetchInProgress = false - @Published var courses: [CourseItem] = [] - @Published var showError: Bool = false + var courses: [CourseItem] = [] + var showError: Bool = false var errorMessage: String? { didSet { withAnimation { diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index e0f6eff05..78d6c6421 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -13,7 +13,7 @@ import Swinject public struct PrimaryCourseDashboardView: View { - @StateObject private var viewModel: PrimaryCourseDashboardViewModel + private var viewModel: PrimaryCourseDashboardViewModel @ViewBuilder let programView: ProgramView private var openDiscoveryPage: () -> Void private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @@ -25,7 +25,7 @@ public struct PrimaryCourseDashboardView: View { programView: ProgramView, openDiscoveryPage: @escaping () -> Void ) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.programView = programView self.openDiscoveryPage = openDiscoveryPage } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index b4e0a5ac6..8cbba4632 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -8,19 +8,19 @@ import Foundation import Core import SwiftUI -import Combine @MainActor -public class PrimaryCourseDashboardViewModel: ObservableObject { - +@Observable +public class PrimaryCourseDashboardViewModel { + var nextPage = 1 var totalPages = 1 - @Published public private(set) var fetchInProgress = true - @Published var enrollments: PrimaryEnrollment? - @Published var showError: Bool = false - @Published var updateNeeded: Bool = false + public private(set) var fetchInProgress = true + var enrollments: PrimaryEnrollment? + var showError: Bool = false + var updateNeeded: Bool = false private var updateShowedOnce: Bool = false - + var errorMessage: String? { didSet { withAnimation { @@ -28,14 +28,14 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { } } } - + let connectivity: ConnectivityProtocol private let interactor: DashboardInteractorProtocol let analytics: DashboardAnalytics let config: ConfigProtocol var storage: CoreStorage let router: DashboardRouter - private var cancellables = Set() + @ObservationIgnored private var observers: [NSObjectProtocol] = [] private let ipadPageSize = 7 private let iphonePageSize = 5 @@ -54,62 +54,65 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { self.config = config self.storage = storage self.router = router - - let enrollmentPublisher = NotificationCenter.default.publisher(for: .onCourseEnrolled) - let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) - let refreshEnrollmentsPublisher = NotificationCenter.default.publisher(for: .refreshEnrollments) - - enrollmentPublisher - .sink { [weak self] _ in - guard let self = self else { return } - Task { - await self.getEnrollments() - } - } - .store(in: &cancellables) - - completionPublisher - .sink { [weak self] _ in - guard let self = self else { return } - DispatchQueue.main.async { - self.updateEnrollmentsIfNeeded() - } + + let enrollmentObserver = NotificationCenter.default.addObserver( + forName: .onCourseEnrolled, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self = self else { return } + Task { + await self.getEnrollments() } - .store(in: &cancellables) - - refreshEnrollmentsPublisher - .sink { [weak self] _ in - guard let self = self else { return } - Task { - await self.getEnrollments() - } + } + + let completionObserver = NotificationCenter.default.addObserver( + forName: .onblockCompletionRequested, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self = self else { return } + self.updateEnrollmentsIfNeeded() + } + + let refreshObserver = NotificationCenter.default.addObserver( + forName: .refreshEnrollments, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self = self else { return } + Task { + await self.getEnrollments() } - .store(in: &cancellables) + } + + observers.append(contentsOf: [enrollmentObserver, completionObserver, refreshObserver]) } func setupNotifications() { - NotificationCenter.default.publisher(for: .onActualVersionReceived) - .receive(on: DispatchQueue.main) - .sink { [weak self] notification in - if let latestVersion = notification.object as? String { - // Save the latest version to storage - self?.storage.latestAvailableAppVersion = latestVersion - - if let info = Bundle.main.infoDictionary { - guard let currentVersion = info["CFBundleShortVersionString"] as? String, - let self else { return } - if currentVersion.isAppVersionGreater(than: latestVersion) == false - && currentVersion != latestVersion { - if self.updateShowedOnce == false { - DispatchQueue.main.async { - self.router.showUpdateRecomendedView() - } - self.updateShowedOnce = true - } + let versionObserver = NotificationCenter.default.addObserver( + forName: .onActualVersionReceived, + object: nil, + queue: .main + ) { [weak self] notification in + guard let self = self else { return } + if let latestVersion = notification.object as? String { + // Save the latest version to storage + self.storage.latestAvailableAppVersion = latestVersion + + if let info = Bundle.main.infoDictionary { + guard let currentVersion = info["CFBundleShortVersionString"] as? String else { return } + if currentVersion.isAppVersionGreater(than: latestVersion) == false + && currentVersion != latestVersion { + if self.updateShowedOnce == false { + self.router.showUpdateRecomendedView() + self.updateShowedOnce = true } } } - }.store(in: &cancellables) + } + } + observers.append(versionObserver) } private func updateEnrollmentsIfNeeded() { @@ -148,4 +151,9 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { func trackDashboardCourseClicked(courseID: String, courseName: String) { analytics.dashboardCourseClicked(courseID: courseID, courseName: courseName) } + + @MainActor + deinit { + observers.forEach { NotificationCenter.default.removeObserver($0) } + } } diff --git a/Discovery/Discovery.xcodeproj/project.pbxproj b/Discovery/Discovery.xcodeproj/project.pbxproj index 9669212f1..957a8682a 100644 --- a/Discovery/Discovery.xcodeproj/project.pbxproj +++ b/Discovery/Discovery.xcodeproj/project.pbxproj @@ -837,7 +837,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -952,7 +952,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1130,7 +1130,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1166,7 +1166,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1265,7 +1265,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1365,7 +1365,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1459,7 +1459,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1552,7 +1552,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift index fe1c32657..e8c7bace7 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift @@ -13,11 +13,14 @@ import WebKit import Theme public struct CourseDetailsView: View { - - @ObservedObject private var viewModel: CourseDetailsViewModel + @Environment(\.colorScheme) var colorScheme @Environment(\.isHorizontal) var isHorizontal - @State private var isOverviewRendering = true + + @State var isProcessing: Bool = true + + private var viewModel: CourseDetailsViewModel + private var title: String private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } private var courseID: String @@ -121,13 +124,13 @@ public struct CourseDetailsView: View { html: courseDetails.overviewHTML, type: .discovery, screenWidth: proxy.size.width - 48), - processing: { rendering in - isOverviewRendering = rendering + processing: { isProcessing in + self.isProcessing = isProcessing } ) .padding(.horizontal, 16) - if isOverviewRendering { + if isProcessing { ProgressBar(size: 40, lineWidth: 8) .padding(.top, 20) .frame(maxWidth: .infinity) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsViewModel.swift index 6f4aa814c..69442e710 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsViewModel.swift @@ -16,12 +16,13 @@ public enum CourseState { } @MainActor -public final class CourseDetailsViewModel: ObservableObject { +@Observable +public final class CourseDetailsViewModel { - @Published var courseDetails: CourseDetails? - @Published private(set) var isShowProgress = false - @Published var showError: Bool = false - @Published var isHorisontal: Bool = false + var courseDetails: CourseDetails? + private(set) var isShowProgress = false + var showError: Bool = false + var isHorisontal: Bool = false var errorMessage: String? { didSet { withAnimation { @@ -63,6 +64,7 @@ public final class CourseDetailsViewModel: ObservableObject { @MainActor func getCourseDetail(courseID: String, withProgress: Bool = true) async { isShowProgress = withProgress + do { if connectivity.isInternetAvaliable { courseDetails = try await interactor.getCourseDetails(courseID: courseID) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index cafe26adb..0be49e77b 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -11,12 +11,9 @@ import OEXFoundation import Theme public struct DiscoveryView: View { - - @StateObject - private var viewModel: DiscoveryViewModel + + @Bindable private var viewModel: DiscoveryViewModel private var router: DiscoveryRouter - @State private var searchQuery: String = "" - @State private var isRefreshing: Bool = false private var sourceScreen: LogistrationSourceScreen @@ -42,9 +39,9 @@ public struct DiscoveryView: View { searchQuery: String? = nil, sourceScreen: LogistrationSourceScreen = .default ) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.router = router - self._searchQuery = State(initialValue: searchQuery ?? "") + viewModel.searchQuery = searchQuery ?? "" self.sourceScreen = sourceScreen } @@ -68,7 +65,7 @@ public struct DiscoveryView: View { Spacer() } .onTapGesture { - router.showDiscoverySearch(searchQuery: searchQuery) + router.showDiscoverySearch(searchQuery: viewModel.searchQuery) viewModel.discoverySearchBarClicked() } .frame(minHeight: 48) @@ -82,7 +79,7 @@ public struct DiscoveryView: View { .stroke(lineWidth: 1) .fill(Theme.Colors.textInputUnfocusedStroke) ).onTapGesture { - router.showDiscoverySearch(searchQuery: searchQuery) + router.showDiscoverySearch(searchQuery: viewModel.searchQuery) viewModel.discoverySearchBarClicked() } .padding(.top, 11.5) @@ -190,9 +187,9 @@ public struct DiscoveryView: View { } .navigationBarHidden(sourceScreen != .startup) .onFirstAppear { - if !(searchQuery.isEmpty) { - router.showDiscoverySearch(searchQuery: searchQuery) - searchQuery = "" + if !(viewModel.searchQuery.isEmpty) { + router.showDiscoverySearch(searchQuery: viewModel.searchQuery) + viewModel.searchQuery = "" } Task { await viewModel.discovery(page: 1) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift index 509ddbd6f..0d97e385d 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift @@ -10,14 +10,17 @@ import Core import SwiftUI @MainActor -public final class DiscoveryViewModel: ObservableObject { - +@Observable +public final class DiscoveryViewModel { + + var searchQuery = "" var nextPage = 1 var totalPages = 1 + var isRefreshing = false private(set) var fetchInProgress = false - @Published var courses: [CourseItem] = [] - @Published var showError: Bool = false + var courses: [CourseItem] = [] + var showError: Bool = false var userloggedIn: Bool { return !(storage.user?.username?.isEmpty ?? true) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift index 7c565bfad..d97d40e38 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift @@ -14,11 +14,10 @@ public struct SearchView: View { @FocusState private var focused: Bool - - @ObservedObject + + @Bindable private var viewModel: SearchViewModel - @State private var animated: Bool = false - + public init(viewModel: SearchViewModel, searchQuery: String? = nil) { self.viewModel = viewModel self.viewModel.searchText = searchQuery ?? "" @@ -102,8 +101,8 @@ public struct SearchView: View { searchHeader(viewModel: viewModel) .padding(.horizontal, 24) .padding(.bottom, 20) - .offset(y: animated ? 0 : 50) - .opacity(animated ? 1 : 0) + .offset(y: viewModel.animated ? 0 : 50) + .opacity(viewModel.animated ? 1 : 0) Spacer() } .padding(.leading, 10) @@ -170,7 +169,7 @@ public struct SearchView: View { .onAppear { DispatchQueue.main.asyncAfter(deadline: .now()) { withAnimation(.easeIn(duration: 0.3)) { - animated = true + viewModel.animated = true } } } diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift index 05c9b7ac4..5484f2ec9 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift @@ -11,18 +11,28 @@ import SwiftUI import Combine @MainActor -public final class SearchViewModel: ObservableObject { +@Observable public final class SearchViewModel { var nextPage = 1 var totalPages = 1 - @Published private(set) var fetchInProgress = false - @Published var isSearchActive = false - @Published var searchResults: [CourseItem] = [] - @Published var showError: Bool = false - @Published var searchText: String = "" + + private(set) var fetchInProgress = false + var isSearchActive = false + var animated: Bool = false + var searchResults: [CourseItem] = [] + var showError: Bool = false + + var searchText: String = "" { + didSet { + handleSearchTextChange(oldValue: oldValue, newValue: searchText) + } + } + private var prevQuery: String = "" private var subscription = Set() private let debounce: Debounce - + + @ObservationIgnored private var searchTask: Task? + var errorMessage: String? { didSet { withAnimation { @@ -50,31 +60,31 @@ public final class SearchViewModel: ObservableObject { self.analytics = analytics self.storage = storage self.debounce = debounce - - $searchText - .debounce(for: debounce.dueTime, scheduler: debounce.scheduler) - .removeDuplicates() - .sink { str in - let term = str - .trimmingCharacters(in: .whitespaces) - Task.detached(priority: .high) { - if !term.isEmpty { - if await term == self.prevQuery { return } - await MainActor.run { - self.nextPage = 1 - } - await self.search(page: self.nextPage, searchTerm: str) - } else { - await MainActor.run { - self.prevQuery = "" - self.searchResults.removeAll() - } - } - } + } + + private func handleSearchTextChange(oldValue: String, newValue: String) { + searchTask?.cancel() + + searchTask = Task { @MainActor in + try? await Task.sleep(for: .milliseconds(500)) + + guard !Task.isCancelled else { return } + + let term = newValue.trimmingCharacters(in: .whitespaces) + + if !term.isEmpty { + if term == prevQuery { return } + + nextPage = 1 + + await search(page: nextPage, searchTerm: newValue) + } else { + prevQuery = "" + searchResults.removeAll() } - .store(in: &subscription) + } } - + @MainActor public func searchCourses(index: Int, searchTerm: String) async { if !fetchInProgress { diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift index 2b7b48199..a7e7ee87d 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift @@ -29,10 +29,9 @@ public enum DiscoveryWebviewType: Equatable { } public struct DiscoveryWebview: View { - @State private var searchQuery: String = "" - @State private var isLoading: Bool = true - - @StateObject private var viewModel: DiscoveryWebviewViewModel + + @Bindable private var viewModel: DiscoveryWebviewViewModel + private var router: DiscoveryRouter private var discoveryType: DiscoveryWebviewType public var pathID: String @@ -40,9 +39,9 @@ public struct DiscoveryWebview: View { private var URLString: String { switch discoveryType { case .discovery: - if !searchQuery.isEmpty { + if !viewModel.searchQuery.isEmpty { let baseURL = viewModel.config.discovery.webview.baseURL ?? "" - return buildQuery(baseURL: baseURL, params: ["q": searchQuery]) + return buildQuery(baseURL: baseURL, params: ["q": viewModel.searchQuery]) } return viewModel.config.discovery.webview.baseURL ?? "" @@ -82,9 +81,9 @@ public struct DiscoveryWebview: View { discoveryType: DiscoveryWebviewType = .discovery, pathID: String = "" ) { - self._viewModel = .init(wrappedValue: viewModel) + self.viewModel = viewModel self.router = router - self._searchQuery = State(initialValue: searchQuery ?? "") + viewModel.searchQuery = searchQuery ?? "" self.discoveryType = discoveryType self.pathID = pathID } @@ -99,7 +98,7 @@ public struct DiscoveryWebview: View { baseURL: "", openFile: {_ in} ), - isLoading: $isLoading, + isLoading: $viewModel.isLoading, refreshCookies: {}, navigationDelegate: viewModel, connectivity: viewModel.connectivity, @@ -107,7 +106,7 @@ public struct DiscoveryWebview: View { ) .accessibilityIdentifier("discovery_webview") - if isLoading || viewModel.showProgress { + if viewModel.isLoading || viewModel.showProgress { HStack(alignment: .center) { ProgressBar( size: 40, @@ -133,7 +132,7 @@ public struct DiscoveryWebview: View { } } - if !viewModel.userloggedIn, !isLoading { + if !viewModel.userloggedIn, !viewModel.isLoading { LogistrationBottomView( ssoEnabled: viewModel.config.uiComponents.samlSSOLoginEnabled ) { buttonAction in diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift index 3cbaf5d77..6bcc26029 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -11,11 +11,15 @@ import SwiftUI import WebKit @MainActor -public final class DiscoveryWebviewViewModel: ObservableObject { - @Published var courseDetails: CourseDetails? - @Published private(set) var showProgress = false - @Published var showError: Bool = false - @Published var webViewError: Bool = false +@Observable +public final class DiscoveryWebviewViewModel { + + var courseDetails: CourseDetails? + private(set) var showProgress = false + var showError: Bool = false + var webViewError: Bool = false + var searchQuery: String = "" + var isLoading: Bool = true var errorMessage: String? { didSet { diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift index dbfc60403..58f9236e4 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift @@ -17,9 +17,8 @@ public enum ProgramViewType: String, Equatable { } public struct ProgramWebviewView: View { - @State private var isLoading: Bool = true - - @StateObject private var viewModel: ProgramWebviewViewModel + @Bindable private var viewModel: ProgramWebviewViewModel + private var router: DiscoveryRouter private var viewType: ProgramViewType public var pathID: String @@ -43,7 +42,7 @@ public struct ProgramWebviewView: View { viewType: ProgramViewType = .program, pathID: String = "" ) { - self._viewModel = .init(wrappedValue: viewModel) + self.viewModel = viewModel self.router = router self.viewType = viewType self.pathID = pathID @@ -60,7 +59,7 @@ public struct ProgramWebviewView: View { openFile: {_ in}, injections: [.colorInversionCss] ), - isLoading: $isLoading, + isLoading: $viewModel.webViewIsLoading, refreshCookies: { await viewModel.updateCookies( force: true @@ -73,7 +72,7 @@ public struct ProgramWebviewView: View { .accessibilityIdentifier("program_webview") let shouldShowProgress = ( - isLoading || + viewModel.webViewIsLoading || viewModel.showProgress || viewModel.updatingCookies ) diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift index b575e8480..6d15e9388 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift @@ -10,13 +10,16 @@ import Core import SwiftUI import WebKit -public final class ProgramWebviewViewModel: ObservableObject, WebviewCookiesUpdateProtocol { - @Published var courseDetails: CourseDetails? - @Published private(set) var showProgress = false - @Published var showError: Bool = false - @Published var webViewError: Bool = false - @Published public var updatingCookies: Bool = false - @Published public var cookiesReady: Bool = false +@Observable +public final class ProgramWebviewViewModel: WebviewCookiesUpdateProtocol { + var courseDetails: CourseDetails? + private(set) var showProgress = false + var showError: Bool = false + var webViewError: Bool = false + var webViewIsLoading: Bool = false + + public var updatingCookies: Bool = false + public var cookiesReady: Bool = false public var errorMessage: String? { didSet { diff --git a/Discussion/Discussion.xcodeproj/project.pbxproj b/Discussion/Discussion.xcodeproj/project.pbxproj index 3daa42fdb..153a365db 100644 --- a/Discussion/Discussion.xcodeproj/project.pbxproj +++ b/Discussion/Discussion.xcodeproj/project.pbxproj @@ -964,7 +964,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -998,7 +998,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1095,7 +1095,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1193,7 +1193,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1285,7 +1285,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1376,7 +1376,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1599,7 +1599,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1712,7 +1712,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift index 5ed34cff0..94559da56 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift @@ -12,18 +12,19 @@ import Combine import Swinject @MainActor +@Observable public class BaseResponsesViewModel { - @Published public var postComments: Post? - @Published public var isShowProgress = false - @Published public var showError = false - @Published public var showAlert = false - @Published public var addCommentsIsVisible = true + public var postComments: Post? + public var isShowProgress = false + public var showError = false + public var showAlert = false + public var addCommentsIsVisible = true internal var comments: [UserComment] = [] public var nextPage = 2 public var totalPages = 1 - @Published public var itemsCount = 0 + public var itemsCount = 0 public var fetchInProgress = false var errorMessage: String? { diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift index 0b943be91..82b89e3fc 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift @@ -18,7 +18,7 @@ public struct ResponsesView: View { private let commentID: String private let parentComment: Post - @ObservedObject private var viewModel: ResponsesViewModel + private var viewModel: ResponsesViewModel @State private var isShowProgress: Bool = true public init( diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift index 4a91a88c4..090798393 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift @@ -10,9 +10,10 @@ import SwiftUI import Core import Combine -public final class ResponsesViewModel: BaseResponsesViewModel, ObservableObject { +@Observable +public final class ResponsesViewModel: BaseResponsesViewModel { - @Published var scrollTrigger: Bool = false + var scrollTrigger: Bool = false private let threadStateSubject: CurrentValueSubject public var isBlackedOut: Bool = false private let analytics: DiscussionAnalytics? diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index b673630bc..6e10bac27 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -16,7 +16,7 @@ public struct ThreadView: View { public let thread: UserThread private var onBackTapped: (() -> Void) = {} - @ObservedObject private var viewModel: ThreadViewModel + private var viewModel: ThreadViewModel @Environment(\.colorScheme) var colorScheme @State private var isShowProgress: Bool = true @State private var commentText: String = "" diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index d33842aa6..9939d5375 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -9,9 +9,10 @@ import Foundation import Combine import Core -public final class ThreadViewModel: BaseResponsesViewModel, ObservableObject { +@Observable +public final class ThreadViewModel: BaseResponsesViewModel { - @Published var scrollTrigger: Bool = false + var scrollTrigger: Bool = false internal let threadStateSubject = CurrentValueSubject(nil) private var cancellable: AnyCancellable? diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift index e630c9b8d..8c61dc2b1 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift @@ -20,9 +20,8 @@ public struct CreateNewThreadView: View { private var courseID: String @Environment(\.colorScheme) var colorScheme - - @ObservedObject - private var viewModel: CreateNewThreadViewModel + + @Bindable private var viewModel: CreateNewThreadViewModel public init( viewModel: CreateNewThreadViewModel, diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift index 688d0c62e..b7db84fb7 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift @@ -9,12 +9,13 @@ import Core import SwiftUI @MainActor -public class CreateNewThreadViewModel: ObservableObject { +@Observable +public class CreateNewThreadViewModel { - @Published private(set) var isShowProgress = false - @Published var showError: Bool = false - @Published var allTopics: [CoursewareTopics] = [] - @Published var selectedTopic: String = "" + private(set) var isShowProgress = false + var showError: Bool = false + var allTopics: [CoursewareTopics] = [] + var selectedTopic: String = "" public var topics: Topics? var errorMessage: String? { diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift index aedba367d..f2804cf45 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift @@ -15,10 +15,10 @@ public struct DiscussionSearchTopicsView: View { @FocusState private var focused: Bool - @ObservedObject private var viewModel: DiscussionSearchTopicsViewModel + @Bindable private var viewModel: DiscussionSearchTopicsViewModel @State private var animated: Bool = false - - public init(viewModel: DiscussionSearchTopicsViewModel) { + + public init(viewModel: DiscussionSearchTopicsViewModel) { self.viewModel = viewModel } @@ -162,7 +162,7 @@ public struct DiscussionSearchTopicsView: View { } } - private func searchHeader(viewModel: DiscussionSearchTopicsViewModel) -> some View { + private func searchHeader(viewModel: DiscussionSearchTopicsViewModel) -> some View { return VStack(alignment: .leading) { Text(DiscussionLocalization.Search.title) .font(Theme.Fonts.displaySmall) @@ -172,8 +172,8 @@ public struct DiscussionSearchTopicsView: View { .foregroundColor(Theme.Colors.textPrimary) }.listRowBackground(Color.clear) } - - private func searchDescription(viewModel: DiscussionSearchTopicsViewModel) -> String { + + private func searchDescription(viewModel: DiscussionSearchTopicsViewModel) -> String { let searchEmptyDescription = DiscussionLocalization.Search.emptyDescription let searchDescription = DiscussionLocalization.searchResultsDescription( viewModel.searchResults.isEmpty @@ -199,7 +199,6 @@ struct DiscussionSearchTopicsView_Previews: PreviewProvider { interactor: DiscussionInteractor.mock, storage: CoreStorageMock(), router: DiscussionRouterMock(), - debounce: .searchDebounce ) DiscussionSearchTopicsView(viewModel: vm) diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift index 641f768c6..98efe4e43 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift @@ -8,27 +8,32 @@ import Foundation import SwiftUI import Core -@preconcurrency import Combine +import Combine @MainActor -public final class DiscussionSearchTopicsViewModel: ObservableObject { - - @Published private(set) var fetchInProgress = false - @Published var isSearchActive = false - @Published var searchResults: [DiscussionPost] = [] - @Published var showError: Bool = false - @Published var searchText: String = "" - +@Observable +public final class DiscussionSearchTopicsViewModel { + + private(set) var fetchInProgress = false + var isSearchActive = false + var searchResults: [DiscussionPost] = [] + var showError: Bool = false + var searchText: String = "" { + didSet { + handleSearchTextChange(searchText) + } + } + private var prevQuery: String = "" private var courseID: String - private var subscription = Set() - @Published private var threads: [UserThread] = [] - + private var threads: [UserThread] = [] + private var nextPage = 1 private var totalPages = 1 - - internal let postStateSubject = CurrentValueSubject(nil) - private var cancellable: AnyCancellable? + + @ObservationIgnored private var searchTask: Task? + // Keep CurrentValueSubject for now since it's passed to router + @ObservationIgnored internal let postStateSubject = CurrentValueSubject(nil) var errorMessage: String? { didSet { @@ -41,25 +46,25 @@ public final class DiscussionSearchTopicsViewModel: ObservableObje let router: DiscussionRouter private let interactor: DiscussionInteractorProtocol private let storage: CoreStorage - private let debounce: Debounce - + private let debounceInterval: TimeInterval + public init( courseID: String, interactor: DiscussionInteractorProtocol, storage: CoreStorage, router: DiscussionRouter, - debounce: Debounce + debounceInterval: TimeInterval = 0.8 ) { self.courseID = courseID self.interactor = interactor self.storage = storage self.router = router - self.debounce = debounce + self.debounceInterval = debounceInterval - cancellable = postStateSubject - .receive(on: RunLoop.main) - .sink(receiveValue: { [weak self] state in - guard let self, let state else { return } + // Setup observer for postStateSubject + Task { + for await state in postStateSubject.values { + guard let state = state else { continue } switch state { case let .followed(id, followed): self.updatePostFollowedState(id: id, followed: followed) @@ -72,33 +77,37 @@ public final class DiscussionSearchTopicsViewModel: ObservableObje case let .reported(id, reported): self.updatePostReportedState(id: id, reported: reported) } - }) - - $searchText - .debounce(for: debounce.dueTime, scheduler: debounce.scheduler) - .removeDuplicates() - .sink { [weak self] str in - guard let self else { return } - let term = str - .trimmingCharacters(in: .whitespaces) - Task.detached(priority: .high) { - if !term.isEmpty { - if await term == self.prevQuery { - return - } - await MainActor.run { - self.nextPage = 1 - } - await self.search(page: self.nextPage, searchTerm: str) - } else { - await MainActor.run { - self.prevQuery = "" - self.searchResults.removeAll() - } - } + } + } + } + + deinit { + searchTask?.cancel() + } + + private func handleSearchTextChange(_ text: String) { + // Cancel previous search task + searchTask?.cancel() + + // Start new debounced search + searchTask = Task { + try? await Task.sleep(nanoseconds: UInt64(debounceInterval * 1_000_000_000)) + + guard !Task.isCancelled else { return } + + let term = text.trimmingCharacters(in: .whitespaces) + + if !term.isEmpty { + if term == prevQuery { + return } + nextPage = 1 + await search(page: nextPage, searchTerm: text) + } else { + prevQuery = "" + searchResults.removeAll() } - .store(in: &subscription) + } } func searchCourses(index: Int, searchTerm: String) async { diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift index 61106f87a..bc9218acc 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift @@ -12,7 +12,7 @@ import Theme public struct DiscussionTopicsView: View { - @StateObject private var viewModel: DiscussionTopicsViewModel + @Bindable private var viewModel: DiscussionTopicsViewModel private let router: DiscussionRouter private let courseID: String @Binding private var coordinate: CGFloat @@ -28,7 +28,7 @@ public struct DiscussionTopicsView: View { viewModel: DiscussionTopicsViewModel, router: DiscussionRouter ) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel self.courseID = courseID self._coordinate = coordinate self._collapsed = collapsed @@ -48,12 +48,13 @@ public struct DiscussionTopicsView: View { viewHeight: $viewHeight ) RefreshProgressView(isShowRefresh: $viewModel.isShowRefresh) + // MARK: - Search fake field if viewModel.isBlackedOut { bannerDiscussionsDisabled } - if let topics = viewModel.discussionTopics, topics.count > 0 { + if !viewModel.discussionTopics.isEmpty { HStack(spacing: 11) { Image(systemName: "magnifyingglass") .foregroundColor(Theme.Colors.textInputTextColor) @@ -91,7 +92,7 @@ public struct DiscussionTopicsView: View { VStack { ZStack(alignment: .top) { VStack { - if let topics = viewModel.discussionTopics { + if !viewModel.discussionTopics.isEmpty { HStack { Text(DiscussionLocalization.Topics.mainCategories) .font(Theme.Fonts.titleMedium) @@ -101,7 +102,7 @@ public struct DiscussionTopicsView: View { Spacer() } HStack(spacing: 8) { - if let allTopics = topics.first(where: { + if let allTopics = viewModel.discussionTopics.first(where: { $0.name == DiscussionLocalization.Topics.allPosts }) { Button(action: { allTopics.action() @@ -119,7 +120,7 @@ public struct DiscussionTopicsView: View { }).cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground) .padding(.trailing, -20) } - if let followed = topics.first(where: { + if let followed = viewModel.discussionTopics.first(where: { $0.name == DiscussionLocalization.Topics.postImFollowing}) { Button(action: { followed.action() @@ -139,7 +140,8 @@ public struct DiscussionTopicsView: View { } }.padding(.bottom, 16) - ForEach(Array(topics.enumerated()), id: \.offset) { _, topic in + ForEach(Array(viewModel.discussionTopics.enumerated()), + id: \.offset) { _, topic in if topic.name != DiscussionLocalization.Topics.allPosts && topic.name != DiscussionLocalization.Topics.postImFollowing { diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift index 3a5dfffd0..5909d8473 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift @@ -11,15 +11,16 @@ import Core // swiftlint:disable function_body_length @MainActor -public final class DiscussionTopicsViewModel: ObservableObject { +@Observable +public final class DiscussionTopicsViewModel { - @Published var topics: Topics? - @Published var isShowProgress = true - @Published var isShowRefresh = false - @Published var showError: Bool = false - @Published var discussionTopics: [DiscussionTopic]? - @Published var courseID: String = "" - @Published private(set) var isBlackedOut: Bool = false + var topics: Topics? + var isShowProgress = true + var isShowRefresh = false + var showError: Bool = false + var discussionTopics: [DiscussionTopic] = [] + var courseID: String = "" + private(set) var isBlackedOut: Bool = false let title: String var errorMessage: String? { @@ -179,12 +180,13 @@ public final class DiscussionTopicsViewModel: ObservableObject { topics = try await interactor.getTopics(courseID: courseID) discussionTopics = generateTopics(topics: topics) + isShowProgress = false isShowRefresh = false } catch { isShowProgress = false isShowRefresh = false - discussionTopics = nil + discussionTopics = [] } } } diff --git a/Discussion/Discussion/Presentation/Posts/PostsView.swift b/Discussion/Discussion/Presentation/Posts/PostsView.swift index f3bfd7dc1..b7fbd9b30 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsView.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsView.swift @@ -12,7 +12,7 @@ import Theme public struct PostsView: View { - @ObservedObject private var viewModel: PostsViewModel + private var viewModel: PostsViewModel @State private var showFilterSheet = false @State private var showSortSheet = false private let router: DiscussionRouter diff --git a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift index 2ac959a93..f36cecdaf 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift @@ -11,21 +11,22 @@ import Combine import Core @MainActor -public final class PostsViewModel: ObservableObject { +@Observable +public final class PostsViewModel { 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 case filter } - @Published private(set) var isShowProgress = false - @Published var showError: Bool = false - @Published var filteredPosts: [DiscussionPost] = [] - @Published var filterTitle: ThreadsFilter = .allThreads { + private(set) var isShowProgress = false + var showError: Bool = false + var filteredPosts: [DiscussionPost] = [] + var filterTitle: ThreadsFilter = .allThreads { willSet { if courseID != nil { resetPosts() @@ -35,7 +36,8 @@ public final class PostsViewModel: ObservableObject { } } } - @Published var sortTitle: SortType = .recentActivity { + + var sortTitle: SortType = .recentActivity { willSet { if courseID != nil { resetPosts() @@ -63,7 +65,7 @@ public final class PostsViewModel: ObservableObject { } public var courseID: String? - @Published var isBlackedOut: Bool? + var isBlackedOut: Bool? var errorMessage: String? { didSet { diff --git a/Downloads/Downloads.xcodeproj/project.pbxproj b/Downloads/Downloads.xcodeproj/project.pbxproj index 2907f8e3d..deca3361c 100644 --- a/Downloads/Downloads.xcodeproj/project.pbxproj +++ b/Downloads/Downloads.xcodeproj/project.pbxproj @@ -564,7 +564,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -604,7 +604,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -878,7 +878,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1006,7 +1006,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1134,7 +1134,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1255,7 +1255,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1375,7 +1375,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1495,7 +1495,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Downloads/Downloads/Presentation/AppDownloadsView.swift b/Downloads/Downloads/Presentation/AppDownloadsView.swift index 13ff61eb7..ce4988551 100644 --- a/Downloads/Downloads/Presentation/AppDownloadsView.swift +++ b/Downloads/Downloads/Presentation/AppDownloadsView.swift @@ -15,8 +15,8 @@ public struct AppDownloadsView: View { @Environment(\.isHorizontal) private var isHorizontal private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - @StateObject private var viewModel: AppDownloadsViewModel - + @Bindable private var viewModel: AppDownloadsViewModel + private func columns() -> [GridItem] { isHorizontal || idiom == .pad ? [ @@ -29,7 +29,7 @@ public struct AppDownloadsView: View { } public init(viewModel: AppDownloadsViewModel) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel } public var body: some View { diff --git a/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift b/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift index 79696e030..0bb71f08c 100644 --- a/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift +++ b/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift @@ -12,17 +12,18 @@ import SwiftUI //swiftlint:disable type_body_length @MainActor -public final class AppDownloadsViewModel: ObservableObject { +@Observable +public final class AppDownloadsViewModel { private var cancellables = Set() private var downloadQueue = [String]() private var isProcessingQueue = false - @Published var courses: [DownloadCoursePreview] = [] - @Published var downloadedSizes: [String: Int64] = [:] - @Published var downloadStates: [String: DownloadState] = [:] - @Published var showError: Bool = false - @Published private(set) var fetchInProgress = false + var courses: [DownloadCoursePreview] = [] + var downloadedSizes: [String: Int64] = [:] + var downloadStates: [String: DownloadState] = [:] + var showError: Bool = false + private(set) var fetchInProgress = false private var courseTasks: [String: [DownloadDataTask]] = [:] private var courseSizes: [String: Int64] = [:] diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 8169808be..fbccc49b5 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -713,7 +713,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -805,7 +805,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -903,7 +903,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -995,7 +995,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1147,7 +1147,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1185,7 +1185,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 75dce720d..b1c3cd828 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -600,7 +600,7 @@ class ScreenAssembly: Assembly { interactor: r.resolve(DiscussionInteractorProtocol.self)!, storage: r.resolve(CoreStorage.self)!, router: r.resolve(DiscussionRouter.self)!, - debounce: .searchDebounce + debounceInterval: 0.8 ) } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index aac17b36d..3c6185c08 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -321,7 +321,7 @@ public class Router: AuthorizationRouter, } public func showDiscussionsSearch(courseID: String, isBlackedOut: Bool) { - let viewModel = Container.shared.resolve(DiscussionSearchTopicsViewModel.self, argument: courseID)! + let viewModel = Container.shared.resolve(DiscussionSearchTopicsViewModel.self, argument: courseID)! let view = DiscussionSearchTopicsView(viewModel: viewModel) diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index 786719f37..f94a3f68e 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -18,12 +18,9 @@ import Theme import OEXFoundation struct MainScreenView: View { - - @State private var disableAllTabs: Bool = false - @State private var updateAvailable: Bool = false - - @ObservedObject private(set) var viewModel: MainScreenViewModel - + + @Bindable private(set) var viewModel: MainScreenViewModel + init(viewModel: MainScreenViewModel) { self.viewModel = viewModel UITabBar.appearance().isTranslucent = false @@ -177,10 +174,10 @@ struct MainScreenView: View { } .onReceive(NotificationCenter.default.publisher(for: .onAppUpgradeAccountSettingsTapped)) { _ in viewModel.selection = .profile - disableAllTabs = true + viewModel.disableAllTabs = true } .onReceive(NotificationCenter.default.publisher(for: .onNewVersionAvaliable)) { _ in - updateAvailable = true + viewModel.updateAvailable = true } .onReceive(NotificationCenter.default.publisher(for: .showDownloadFailed)) { downloads in if let downloads = downloads.object as? [DownloadDataTask] { @@ -190,7 +187,7 @@ struct MainScreenView: View { } } .onChange(of: viewModel.selection) { _ in - if disableAllTabs { + if viewModel.disableAllTabs { viewModel.selection = .profile } } @@ -220,7 +217,7 @@ struct MainScreenView: View { } } .accentColor(Theme.Colors.accentXColor) - if updateAvailable { + if viewModel.updateAvailable { UpdateNotificationView(config: viewModel.config) } } diff --git a/OpenEdX/View/MainScreenViewModel.swift b/OpenEdX/View/MainScreenViewModel.swift index 8d0e956b8..3b896f6eb 100644 --- a/OpenEdX/View/MainScreenViewModel.swift +++ b/OpenEdX/View/MainScreenViewModel.swift @@ -24,7 +24,8 @@ public enum MainTab { } @MainActor -final class MainScreenViewModel: ObservableObject { +@Observable +final class MainScreenViewModel { private let analytics: MainScreenAnalytics let config: ConfigProtocol @@ -38,8 +39,10 @@ final class MainScreenViewModel: ObservableObject { private var cancellables = Set() private var postLoginData: PostLoginData? - @Published var selection: MainTab = .dashboard - @Published var showRegisterBanner: Bool = false + var selection: MainTab = .dashboard + var showRegisterBanner: Bool = false + var disableAllTabs = false + var updateAvailable = false init(analytics: MainScreenAnalytics, config: ConfigProtocol, diff --git a/Podfile.lock b/Podfile.lock index 92ba93fc7..ab2424669 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -36,4 +36,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 2d71ad797d49fa32b47c3315b92159de82824103 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index 5fb306ab5..187296ecd 100644 --- a/Profile/Profile.xcodeproj/project.pbxproj +++ b/Profile/Profile.xcodeproj/project.pbxproj @@ -907,7 +907,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -942,7 +942,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1039,7 +1039,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1137,7 +1137,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1229,7 +1229,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1320,7 +1320,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1543,7 +1543,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1656,7 +1656,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift index f1aae91ef..e9d8d9621 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift @@ -10,8 +10,8 @@ import Theme import Core public struct CoursesToSyncView: View { - - @ObservedObject + + @Bindable private var viewModel: DatesAndCalendarViewModel @Environment(\.isHorizontal) private var isHorizontal @@ -80,6 +80,9 @@ public struct CoursesToSyncView: View { } .ignoresSafeArea(.all, edges: .horizontal) } + .onChange(of: viewModel.hideInactiveCourses) { _, hide in + viewModel.profileStorage.hideInactiveCourses = hide + } } private var coursesList: some View { diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift index 6539ea081..8dbf5c7c9 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift @@ -10,8 +10,8 @@ import Theme import Core public struct DatesAndCalendarView: View { - - @ObservedObject + + @Bindable private var viewModel: DatesAndCalendarViewModel @State private var screenDimmed: Bool = false diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift index 9549f53fa..84f870571 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift @@ -6,7 +6,6 @@ // import SwiftUI -import Combine import EventKit import Theme import BranchSDK @@ -17,35 +16,36 @@ import OEXFoundation // MARK: - DatesAndCalendarViewModel @MainActor -public final class DatesAndCalendarViewModel: ObservableObject { - @Published var showCalendaAccessDenied: Bool = false - @Published var showDisableCalendarSync: Bool = false - @Published var showError: Bool = false - @Published var openNewCalendarView: Bool = false +@Observable +public final class DatesAndCalendarViewModel { + var showCalendaAccessDenied: Bool = false + var showDisableCalendarSync: Bool = false + var showError: Bool = false + var openNewCalendarView: Bool = false - @Published var accountSelection: DropDownPicker.DownPickerOption? = .init( + var accountSelection: DropDownPicker.DownPickerOption? = .init( title: ProfileLocalization.Calendar.Dropdown.icloud ) - @Published var calendarName: String = "" - @Published var oldCalendarName: String = "" - @Published var colorSelection: DropDownPicker.DownPickerOption? = .init(color: .accent) - @Published var oldColorSelection: DropDownPicker.DownPickerOption? = .init(color: .accent) + var calendarName: String = "" + var oldCalendarName: String = "" + var colorSelection: DropDownPicker.DownPickerOption? = .init(color: .accent) + var oldColorSelection: DropDownPicker.DownPickerOption? = .init(color: .accent) - @Published var assignmentStatus: AssignmentStatus = .synced - @Published var courseCalendarSync: Bool = true - @Published var reconnectRequired: Bool = false - @Published var openChangeSyncView: Bool = false - @Published var syncingCoursesCount: Int = 0 + var assignmentStatus: AssignmentStatus = .synced + var courseCalendarSync: Bool = true + var reconnectRequired: Bool = false + var openChangeSyncView: Bool = false + var syncingCoursesCount: Int = 0 - @Published var coursesForSync = [CourseForSync]() + var coursesForSync = [CourseForSync]() private var coursesForSyncBeforeChanges = [CourseForSync]() private(set) var coursesForDeleting = [CourseForSync]() private(set) var coursesForAdding = [CourseForSync]() - @Published var synced: Bool = true - @Published var hideInactiveCourses: Bool = false + var synced: Bool = true + var hideInactiveCourses: Bool = false var errorMessage: String? { didSet { @@ -74,12 +74,11 @@ public final class DatesAndCalendarViewModel: ObservableObject { var router: ProfileRouter private var interactor: ProfileInteractorProtocol - @Published var profileStorage: ProfileStorage + var profileStorage: ProfileStorage private var persistence: ProfilePersistenceProtocol private var calendarManager: CalendarManagerProtocol private var connectivity: ConnectivityProtocol - - private var cancellables = Set() + var calendarNameHint: String public init( @@ -120,27 +119,7 @@ public final class DatesAndCalendarViewModel: ObservableObject { } self.courseCalendarSync = calendarSettings.courseCalendarSync self.hideInactiveCourses = profileStorage.hideInactiveCourses ?? false - - $hideInactiveCourses - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] hide in - guard let self = self else { return } - self.profileStorage.hideInactiveCourses = hide - }) - .store(in: &cancellables) - - $courseCalendarSync - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] sync in - guard let self = self else { return } - if !sync { - Task { - await self.showDisableCalendarSync() - } - } - }) - .store(in: &cancellables) - + updateCoursesCount() } diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift index ed4e27b8a..b303d4048 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift @@ -26,7 +26,7 @@ struct NewCalendarView: View { } } - @ObservedObject + @Bindable private var viewModel: DatesAndCalendarViewModel @Environment(\.isHorizontal) private var isHorizontal private var beginSyncingTapped: (() -> Void) = {} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift index be28cc458..691a795bc 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift @@ -10,8 +10,8 @@ import Theme import Core public struct SyncCalendarOptionsView: View { - - @ObservedObject + + @Bindable private var viewModel: DatesAndCalendarViewModel @State private var screenDimmed: Bool = false @@ -198,9 +198,12 @@ public struct SyncCalendarOptionsView: View { await viewModel.fetchCourses() } } - .onChange(of: viewModel.courseCalendarSync) { sync in + .onChange(of: viewModel.courseCalendarSync) { _, sync in if !sync { screenDimmed = true + withAnimation(.bouncy(duration: 0.3)) { + viewModel.showDisableCalendarSync = true + } } } .onAppear { diff --git a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift index 9b2ad9525..29da3ca1e 100644 --- a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift +++ b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift @@ -11,8 +11,8 @@ import OEXFoundation import Theme public struct DeleteAccountView: View { - - @ObservedObject + + @Bindable private var viewModel: DeleteAccountViewModel public init(viewModel: DeleteAccountViewModel) { diff --git a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountViewModel.swift b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountViewModel.swift index 9ffb9cb1b..6d06b74c2 100644 --- a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountViewModel.swift +++ b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountViewModel.swift @@ -10,10 +10,11 @@ import Core import SwiftUI @MainActor -public final class DeleteAccountViewModel: ObservableObject { +@Observable +public final class DeleteAccountViewModel { - @Published private(set) var isShowProgress = false - @Published var showError: Bool = false + private(set) var isShowProgress = false + var showError: Bool = false var errorMessage: String? { didSet { withAnimation { @@ -22,8 +23,8 @@ public final class DeleteAccountViewModel: ObservableObject { } } - @Published var password = "" - @Published var incorrectPassword: Bool = false + var password = "" + var incorrectPassword: Bool = false private let interactor: ProfileInteractorProtocol public let router: ProfileRouter diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift index e82df4e6a..3670fb05b 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift @@ -12,7 +12,7 @@ import Theme public struct EditProfileView: View { - @ObservedObject public var viewModel: EditProfileViewModel + @Bindable public var viewModel: EditProfileViewModel @State private var showingImagePicker = false @State private var showingBottomSheet = false @@ -119,14 +119,19 @@ public struct EditProfileView: View { } } } - .onReceive(viewModel.yearsConfiguration.$text - .combineLatest(viewModel.countriesConfiguration.$text, - viewModel.spokenLanguageConfiguration.$text), - perform: { _ in + .onChange(of: viewModel.yearsConfiguration.text) { _, _ in viewModel.checkChanges() viewModel.checkProfileType() - }) - .onChange(of: viewModel.profileChanges) { _ in + } + .onChange(of: viewModel.countriesConfiguration.text) { _, _ in + viewModel.checkChanges() + viewModel.checkProfileType() + } + .onChange(of: viewModel.spokenLanguageConfiguration.text) { _, _ in + viewModel.checkChanges() + viewModel.checkProfileType() + } + .onChange(of: viewModel.profileChanges) { _, _ in viewModel.checkChanges() viewModel.checkProfileType() } diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift index d8a1b3974..04f178c79 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift @@ -18,16 +18,17 @@ public struct Changes: Equatable, Sendable { } @MainActor -public class EditProfileViewModel: ObservableObject { +@Observable +public class EditProfileViewModel { + + private(set) var userModel: UserProfile + private(set) var selectedCountry: PickerItem? + private(set) var selectedSpokeLanguage: PickerItem? + private(set) var selectedYearOfBirth: PickerItem? - @Published private(set) var userModel: UserProfile - @Published private(set) var selectedCountry: PickerItem? - @Published private(set) var selectedSpokeLanguage: PickerItem? - @Published private(set) var selectedYearOfBirth: PickerItem? - var profileDidEdit: (((UserProfile?, UIImage?)) -> Void)? var oldAvatar: UIImage? - + private let minimumFullAccountAge = 13 private let currentYear = Calendar.current.component(.year, from: Date()) public let profileTypes: [ProfileType] = [.full, .limited] @@ -46,7 +47,6 @@ public class EditProfileViewModel: ObservableObject { ProfileLocalization.Edit.Fields.spokenLangugae ) - @Published public var profileChanges: Changes = .init( shortBiography: "", profileType: .limited, @@ -55,14 +55,14 @@ public class EditProfileViewModel: ObservableObject { isAvatarSaved: false ) - @Published public var inputImage: UIImage? + public var inputImage: UIImage? private(set) var isYongUser: Bool = false private(set) var isEditable: Bool = true - @Published var isChanged = false - @Published private(set) var isShowProgress = false - @Published var showError: Bool = false - @Published var showAlert: Bool = false + var isChanged = false + private(set) var isShowProgress = false + var showError: Bool = false + var showAlert: Bool = false var errorMessage: String? { didSet { @@ -252,7 +252,7 @@ public class EditProfileViewModel: ObservableObject { profileChanges.isAvatarSaved = true } checkChanges() - + if isChanged { if !parameters.isEmpty { isShowProgress = true diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index b1c2e6c6d..98f404610 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -13,10 +13,10 @@ import OEXFoundation public struct ProfileView: View { - @StateObject private var viewModel: ProfileViewModel + @Bindable private var viewModel: ProfileViewModel public init(viewModel: ProfileViewModel) { - self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.viewModel = viewModel } public var body: some View { diff --git a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift index 123c31d26..395e3b613 100644 --- a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift +++ b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift @@ -10,12 +10,13 @@ import Core import SwiftUI @MainActor -public final class ProfileViewModel: ObservableObject { +@Observable +public final class ProfileViewModel { - @Published public var userModel: UserProfile? - @Published public var updatedAvatar: UIImage? - @Published private(set) var isShowProgress = false - @Published var showError: Bool = false + public var userModel: UserProfile? + public var updatedAvatar: UIImage? + private(set) var isShowProgress = false + var showError: Bool = false var errorMessage: String? { didSet { withAnimation { diff --git a/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift b/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift index 986426d87..799df753d 100644 --- a/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift +++ b/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift @@ -20,7 +20,7 @@ struct ProfileSupportInfoView: View { let title: String } - @ObservedObject var viewModel: SettingsViewModel + var viewModel: SettingsViewModel var body: some View { Text(ProfileLocalization.supportInfo) diff --git a/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift index da7e80fd6..1446a6e20 100644 --- a/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift +++ b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift @@ -14,7 +14,7 @@ import Theme public struct UserProfileView: View { @Environment(\.dismiss) private var dismiss - @ObservedObject private var viewModel: UserProfileViewModel + private var viewModel: UserProfileViewModel public var isSheet: Bool diff --git a/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift index 6a723c800..bad1ce1c2 100644 --- a/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift +++ b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift @@ -8,11 +8,12 @@ import Core import SwiftUI -public class UserProfileViewModel: ObservableObject { +@Observable +public class UserProfileViewModel { - @Published public var userModel: UserProfile? - @Published private(set) var isShowProgress = false - @Published var showError: Bool = false + public var userModel: UserProfile? + private(set) var isShowProgress = false + var showError: Bool = false var errorMessage: String? { didSet { withAnimation { diff --git a/Profile/Profile/Presentation/Settings/ManageAccountView.swift b/Profile/Profile/Presentation/Settings/ManageAccountView.swift index 154eddbcf..4dfa936e2 100644 --- a/Profile/Profile/Presentation/Settings/ManageAccountView.swift +++ b/Profile/Profile/Presentation/Settings/ManageAccountView.swift @@ -11,8 +11,8 @@ import OEXFoundation import Theme public struct ManageAccountView: View { - - @ObservedObject + + @Bindable private var viewModel: ManageAccountViewModel @Environment(\.isHorizontal) private var isHorizontal diff --git a/Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift b/Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift index 16415b17f..571c9e924 100644 --- a/Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift +++ b/Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift @@ -10,12 +10,13 @@ import Core import SwiftUI @MainActor -public final class ManageAccountViewModel: ObservableObject { +@Observable +public final class ManageAccountViewModel { - @Published public var userModel: UserProfile? - @Published public var updatedAvatar: UIImage? - @Published private(set) var isShowProgress = false - @Published var showError: Bool = false + public var userModel: UserProfile? + public var updatedAvatar: UIImage? + private(set) var isShowProgress = false + var showError: Bool = false var errorMessage: String? { didSet { withAnimation { diff --git a/Profile/Profile/Presentation/Settings/SettingsView.swift b/Profile/Profile/Presentation/Settings/SettingsView.swift index b1410b656..cad921b47 100644 --- a/Profile/Profile/Presentation/Settings/SettingsView.swift +++ b/Profile/Profile/Presentation/Settings/SettingsView.swift @@ -13,7 +13,6 @@ import Theme public struct SettingsView: View { - @ObservedObject private var viewModel: SettingsViewModel @Environment(\.isHorizontal) private var isHorizontal diff --git a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift index 3b4a2c7f9..2aae0526b 100644 --- a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift +++ b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift @@ -11,11 +11,12 @@ import SwiftUI import Combine @MainActor -public final class SettingsViewModel: ObservableObject { +@Observable +public final class SettingsViewModel { - @Published private(set) var isShowProgress = false - @Published var showError: Bool = false - @Published var wifiOnly: Bool { + private(set) var isShowProgress = false + var showError: Bool = false + var wifiOnly: Bool { willSet { if newValue != wifiOnly { userSettings.wifiOnly = newValue @@ -26,7 +27,7 @@ public final class SettingsViewModel: ObservableObject { } } - @Published var selectedQuality: StreamingQuality { + var selectedQuality: StreamingQuality { willSet { if newValue != selectedQuality { userSettings.streamingQuality = newValue @@ -53,9 +54,9 @@ public final class SettingsViewModel: ObservableObject { case updateRequired } - @Published var versionState: VersionState = .actual - @Published var currentVersion: String = "" - @Published var latestVersion: String = "" + var versionState: VersionState = .actual + var currentVersion: String = "" + var latestVersion: String = "" var errorMessage: String? { didSet { @@ -65,7 +66,7 @@ public final class SettingsViewModel: ObservableObject { } } - @Published private(set) var userSettings: UserSettings + private(set) var userSettings: UserSettings private let interactor: ProfileInteractorProtocol private let downloadManager: DownloadManagerProtocol diff --git a/Profile/Profile/Presentation/Settings/VideoQualityView.swift b/Profile/Profile/Presentation/Settings/VideoQualityView.swift index 8f662976d..053959c47 100644 --- a/Profile/Profile/Presentation/Settings/VideoQualityView.swift +++ b/Profile/Profile/Presentation/Settings/VideoQualityView.swift @@ -13,7 +13,6 @@ import Theme public struct VideoQualityView: View { - @ObservedObject private var viewModel: SettingsViewModel @Environment(\.isHorizontal) private var isHorizontal diff --git a/Profile/Profile/Presentation/Settings/VideoSettingsView.swift b/Profile/Profile/Presentation/Settings/VideoSettingsView.swift index a8c29bcc0..058c6d28d 100644 --- a/Profile/Profile/Presentation/Settings/VideoSettingsView.swift +++ b/Profile/Profile/Presentation/Settings/VideoSettingsView.swift @@ -11,8 +11,7 @@ import Theme public struct VideoSettingsView: View { - @ObservedObject - private var viewModel: SettingsViewModel + @Bindable private var viewModel: SettingsViewModel @Environment(\.isHorizontal) private var isHorizontal public init(viewModel: SettingsViewModel) { diff --git a/WhatsNew/WhatsNew.xcodeproj/project.pbxproj b/WhatsNew/WhatsNew.xcodeproj/project.pbxproj index d1456fe36..5cefbe959 100644 --- a/WhatsNew/WhatsNew.xcodeproj/project.pbxproj +++ b/WhatsNew/WhatsNew.xcodeproj/project.pbxproj @@ -647,7 +647,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -686,7 +686,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -832,7 +832,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -958,7 +958,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1084,7 +1084,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1203,7 +1203,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1321,7 +1321,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1439,7 +1439,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift b/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift index feb9e9877..02cf329c1 100644 --- a/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift +++ b/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift @@ -12,15 +12,12 @@ import Theme public struct WhatsNewView: View { private let router: WhatsNewRouter - - @ObservedObject - private var viewModel: WhatsNewViewModel - + + @Bindable private var viewModel: WhatsNewViewModel + @Environment(\.isHorizontal) private var isHorizontal - - @State var index = 0 - + public init(router: WhatsNewRouter, viewModel: WhatsNewViewModel) { self.router = router self.viewModel = viewModel @@ -32,7 +29,7 @@ public struct WhatsNewView: View { Theme.Colors.background .ignoresSafeArea() adaptiveStack(isHorizontal: isHorizontal) { - TabView(selection: $index) { + TabView(selection: $viewModel.viewIndex) { ForEach(Array(viewModel.newItems.enumerated()), id: \.offset) { _, new in adaptiveStack(isHorizontal: isHorizontal) { ZStack(alignment: .center) { @@ -94,9 +91,9 @@ public struct WhatsNewView: View { HStack(spacing: 36) { WhatsNewNavigationButton(type: .previous, action: { - if index != 0 { + if viewModel.viewIndex != 0 { withAnimation(.linear(duration: 0.3)) { - index -= 1 + viewModel.viewIndex -= 1 } } }) @@ -105,9 +102,9 @@ public struct WhatsNewView: View { WhatsNewNavigationButton( type: viewModel.index < viewModel.newItems.count - 1 ? .next : .done, action: { - if index < viewModel.newItems.count - 1 { + if viewModel.viewIndex < viewModel.newItems.count - 1 { withAnimation(.linear(duration: 0.3)) { - index += 1 + viewModel.viewIndex += 1 } } else { router.showMainOrWhatsNewScreen( @@ -140,7 +137,7 @@ public struct WhatsNewView: View { .accessibilityIdentifier("whatsnew_pagecontrol") } - }.onChange(of: index) { ind in + }.onChange(of: viewModel.viewIndex) { ind in withAnimation(.linear(duration: 0.3)) { viewModel.index = ind } diff --git a/WhatsNew/WhatsNew/Presentation/WhatsNewViewModel.swift b/WhatsNew/WhatsNew/Presentation/WhatsNewViewModel.swift index 8c9086b3a..157c092d5 100644 --- a/WhatsNew/WhatsNew/Presentation/WhatsNewViewModel.swift +++ b/WhatsNew/WhatsNew/Presentation/WhatsNewViewModel.swift @@ -10,9 +10,12 @@ import Core import Swinject @MainActor -public class WhatsNewViewModel: ObservableObject { - @Published var index: Int = 0 - @Published var newItems: [WhatsNewPage] = [] +@Observable public class WhatsNewViewModel { + + var index: Int = 0 + var viewIndex: Int = 0 + var newItems: [WhatsNewPage] = [] + private let storage: WhatsNewStorage var sourceScreen: LogistrationSourceScreen let analytics: WhatsNewAnalytics From f06d232cfe514163c610190d3c30e647ba95c239 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 2 Dec 2025 10:54:25 +0200 Subject: [PATCH 21/51] fix: updates deployment target, adjusts progress view. Updates the iOS deployment target to version 17.0 in the project settings. Refactors the course outline and progress view to handle loading states correctly. --- Core/Core.xcodeproj/project.pbxproj | 16 ++++++++-------- .../CourseOutlineAndProgressView.swift | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index d9f070527..a5275c669 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -1413,7 +1413,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1533,7 +1533,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1792,7 +1792,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1889,7 +1889,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1991,7 +1991,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2088,7 +2088,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2249,7 +2249,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2287,7 +2287,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift b/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift index a2416dac9..392b88541 100644 --- a/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift +++ b/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift @@ -109,13 +109,13 @@ public struct CourseOutlineAndProgressView: View { public var body: some View { ZStack(alignment: .top) { // MARK: - RETURN THIS! -// if viewModelProgress.isLoading || viewModelContainer.isShowRefresh { -// HStack(alignment: .center) { -// ProgressBar(size: 40, lineWidth: 8) -// .padding(.top, 200) -// .padding(.horizontal) -// } -// } else { + if viewModelProgress.isLoading || viewModelContainer.isShowRefresh { + HStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } + } else { // MARK: - RETURN THIS! GeometryReader { _ in VStack(alignment: .center) { @@ -237,7 +237,7 @@ public struct CourseOutlineAndProgressView: View { } } .frameLimit() -// } + } } .background( Theme.Colors.background From 260066812a3d557168a48e80b150cfec44d37d8c Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 9 Dec 2025 10:29:56 +0200 Subject: [PATCH 22/51] fix: webview content display --- Core/Core/View/Base/WebUnitView.swift | 2 +- Core/Core/View/Base/WebUnitViewModel.swift | 9 +++++---- .../Presentation/Unit/Subviews/WebView.swift | 20 ++++++++++++++++++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Core/Core/View/Base/WebUnitView.swift b/Core/Core/View/Base/WebUnitView.swift index e3a0a9eab..06f935ef9 100644 --- a/Core/Core/View/Base/WebUnitView.swift +++ b/Core/Core/View/Base/WebUnitView.swift @@ -11,7 +11,7 @@ import Theme public struct WebUnitView: View { - private var viewModel: WebUnitViewModel + @Bindable private var viewModel: WebUnitViewModel @State private var isWebViewLoading = false private var url: String diff --git a/Core/Core/View/Base/WebUnitViewModel.swift b/Core/Core/View/Base/WebUnitViewModel.swift index 526dfadd9..8cd41688d 100644 --- a/Core/Core/View/Base/WebUnitViewModel.swift +++ b/Core/Core/View/Base/WebUnitViewModel.swift @@ -8,18 +8,19 @@ import Foundation import SwiftUI +@MainActor @Observable public final class WebUnitViewModel: WebviewCookiesUpdateProtocol { - + public let authInteractor: AuthInteractorProtocol let config: ConfigProtocol let syncManager: OfflineSyncManagerProtocol - + public var updatingCookies: Bool = false public var cookiesReady: Bool = false public var showError: Bool = false private var retryCount = 1 - + public var errorMessage: String? { didSet { withAnimation { @@ -27,7 +28,7 @@ public final class WebUnitViewModel: WebviewCookiesUpdateProtocol { } } } - + public init( authInteractor: AuthInteractorProtocol, config: ConfigProtocol, diff --git a/Course/Course/Presentation/Unit/Subviews/WebView.swift b/Course/Course/Presentation/Unit/Subviews/WebView.swift index 935884a28..30b8e0ae4 100644 --- a/Course/Course/Presentation/Unit/Subviews/WebView.swift +++ b/Course/Course/Presentation/Unit/Subviews/WebView.swift @@ -16,13 +16,31 @@ struct WebView: View { let injections: [WebviewInjection] let blockID: String var roundedBackgroundEnabled: Bool = true + @State private var viewModel: WebUnitViewModel + + init( + url: String, + localUrl: String?, + injections: [WebviewInjection], + blockID: String, + roundedBackgroundEnabled: Bool = true, + viewModel: WebUnitViewModel? = nil + ) { + self.url = url + self.localUrl = localUrl + self.injections = injections + self.blockID = blockID + self.roundedBackgroundEnabled = roundedBackgroundEnabled + let resolvedViewModel = viewModel ?? Container.shared.resolve(WebUnitViewModel.self)! + self._viewModel = State(initialValue: resolvedViewModel) + } var body: some View { VStack(spacing: 0) { WebUnitView( url: url, dataUrl: localUrl, - viewModel: Container.shared.resolve(WebUnitViewModel.self)!, + viewModel: viewModel, connectivity: Connectivity(config: ConfigMock()), injections: injections, blockID: blockID From 8326660162ad2f2404708f13e60a2c9596d4f261 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Mon, 15 Dec 2025 16:23:46 +0200 Subject: [PATCH 23/51] fix: main view progress bar fix --- Dashboard/Dashboard/Presentation/ListDashboardView.swift | 2 +- .../Dashboard/Presentation/PrimaryCourseDashboardView.swift | 1 + .../Presentation/PrimaryCourseDashboardViewModel.swift | 2 +- .../Discovery/Presentation/NativeDiscovery/DiscoveryView.swift | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dashboard/Dashboard/Presentation/ListDashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift index 7c7da861c..4b38c7c81 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -144,7 +144,7 @@ public struct ListDashboardView: View { } } } - .onFirstAppear { + .onAppear { Task { await viewModel.getMyCourses(page: 1) } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 78d6c6421..3de1968f1 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -193,6 +193,7 @@ public struct PrimaryCourseDashboardView: View { } .onAppear { viewModel.updateNeeded = true + viewModel.updateEnrollmentsIfNeeded() } .background( Theme.Colors.background diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index 8cbba4632..140ea058a 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -115,7 +115,7 @@ public class PrimaryCourseDashboardViewModel { observers.append(versionObserver) } - private func updateEnrollmentsIfNeeded() { + func updateEnrollmentsIfNeeded() { guard updateNeeded else { return } Task { await getEnrollments() diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index 0be49e77b..eb08a4131 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -186,7 +186,7 @@ public struct DiscoveryView: View { } } .navigationBarHidden(sourceScreen != .startup) - .onFirstAppear { + .onAppear { if !(viewModel.searchQuery.isEmpty) { router.showDiscoverySearch(searchQuery: viewModel.searchQuery) viewModel.searchQuery = "" From e096eb7387c6d60b6b4d336670ee87f04efe2b9b Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 16 Dec 2025 13:55:51 +0200 Subject: [PATCH 24/51] fix: main screen view updates --- OpenEdX/DI/ScreenAssembly.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index b1c3cd828..6da157ace 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -151,8 +151,9 @@ class ScreenAssembly: Assembly { storage: r.resolve(CoreStorage.self)! ) } - - container.register(DiscoveryWebviewViewModel.self) { @MainActor r, sourceScreen in + .inObjectScope(.weak) + + container.register(DiscoveryWebviewViewModel.self) { @MainActor r, sourceScreen in DiscoveryWebviewViewModel( router: r.resolve(DiscoveryRouter.self)!, config: r.resolve(ConfigProtocol.self)!, @@ -223,7 +224,8 @@ class ScreenAssembly: Assembly { router: r.resolve(DashboardRouter.self)! ) } - + .inObjectScope(.container) + container.register(AllCoursesViewModel.self) { @MainActor r in AllCoursesViewModel( interactor: r.resolve(DashboardInteractorProtocol.self)!, From abd7068c0725ce9963e616084a429211463324a0 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 16 Dec 2025 15:33:39 +0200 Subject: [PATCH 25/51] fix: lost change --- .../Dashboard/Presentation/PrimaryCourseDashboardView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 3de1968f1..78d6c6421 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -193,7 +193,6 @@ public struct PrimaryCourseDashboardView: View { } .onAppear { viewModel.updateNeeded = true - viewModel.updateEnrollmentsIfNeeded() } .background( Theme.Colors.background From 5c6d6dc31839dd9bf0e4192c3e2bcfafeefdea87 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 17 Dec 2025 11:33:46 +0200 Subject: [PATCH 26/51] fix: discovery fix --- .../Presentation/NativeDiscovery/DiscoveryView.swift | 6 ++++-- .../Presentation/NativeDiscovery/DiscoveryViewModel.swift | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index eb08a4131..3d751f6b7 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -129,9 +129,11 @@ public struct DiscoveryView: View { VStack(alignment: .center) { ProgressBar(size: 40, lineWidth: 8) .padding(.top, 20) - }.frame(maxWidth: .infinity, + } + .frame(maxWidth: .infinity, maxHeight: .infinity) } + VStack {}.frame(height: 40) } .frameLimit(width: proxy.size.width) @@ -186,7 +188,7 @@ public struct DiscoveryView: View { } } .navigationBarHidden(sourceScreen != .startup) - .onAppear { + .onFirstAppear { if !(viewModel.searchQuery.isEmpty) { router.showDiscoverySearch(searchQuery: viewModel.searchQuery) viewModel.searchQuery = "" diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift index 0d97e385d..deac9f8f8 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift @@ -85,6 +85,7 @@ public final class DiscoveryViewModel { await courses += try interactor.discovery(page: page) } self.nextPage += 1 + if !courses.isEmpty { totalPages = courses[0].numPages } From 0e947fd531577c26475c343ac7946f6653553853 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Mon, 22 Dec 2025 11:27:22 +0200 Subject: [PATCH 27/51] fix: video progress update --- .../Container/CourseContainerViewModel.swift | 409 ++++++++++-------- .../Content/CourseContentView.swift | 5 + .../CourseOutlineAndProgressView.swift | 6 +- .../Video/PlayerServiceProtocol.swift | 2 +- OpenEdX/DI/ScreenAssembly.swift | 1 + 5 files changed, 236 insertions(+), 187 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 6658a10a2..a23723f21 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -43,7 +43,7 @@ extension CourseTab { return CourseLocalization.CourseContainer.handouts } } - + public var image: Image { switch self { case .course: @@ -67,7 +67,7 @@ extension CourseTab { //swiftlint:disable type_body_length file_length @MainActor @Observable public final class CourseContainerViewModel: BaseCourseViewModel { - + public var selection: Int var selectedTab: ContentTab = .all var isShowProgress = false @@ -93,11 +93,12 @@ extension CourseTab { var courseDeadlines: CourseDates? private(set) var assignmentSectionsData: [AssignmentSection] = [] private(set) var realDownloadedFilesSize: Int = 0 - + private var isRefreshingVideoProgress = false + var tabBarIndex = 0 - + let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) - + var errorMessage: String? { didSet { withAnimation { @@ -105,31 +106,31 @@ extension CourseTab { } } } - + let router: CourseRouter let config: ConfigProtocol let connectivity: ConnectivityProtocol - + let isActive: Bool? let courseStart: Date? let courseEnd: Date? let enrollmentStart: Date? let enrollmentEnd: Date? let lastVisitedBlockID: String? - + var courseDownloadTasks: [DownloadDataTask] = [] private(set) var waitingDownloads: [CourseBlock]? - + private let interactor: CourseInteractorProtocol private let authInteractor: AuthInteractorProtocol let analytics: CourseAnalytics let coreAnalytics: CoreAnalytics private(set) var storage: CourseStorage - + private let cellularFileSizeLimit: Int = 100 * 1024 * 1024 var courseHelper: CourseDownloadHelperProtocol - + public init( interactor: CourseInteractorProtocol, authInteractor: AuthInteractorProtocol, @@ -172,7 +173,7 @@ extension CourseTab { self.courseHelper.videoQuality = storage.userSettings?.downloadQuality ?? .auto addObservers() } - + func updateCourseIfNeeded(courseID: String) async { guard !isShowRefresh, !isShowProgress else { return @@ -185,25 +186,25 @@ extension CourseTab { await getCourseBlocks(courseID: courseID, withProgress: true) } } - + func openLastVisitedBlock() { guard let continueWith = continueWith, let courseStructure = courseStructure else { return } let chapter = courseStructure.childs[continueWith.chapterIndex] let sequential = chapter.childs[continueWith.sequentialIndex] let continueUnit = sequential.childs[continueWith.verticalIndex] - + var continueBlock: CourseBlock? continueUnit.childs.forEach { block in if block.id == continueWith.lastVisitedBlockId { continueBlock = block } } - + trackResumeCourseClicked( blockId: continueBlock?.id ?? "" ) - + router.showCourseUnit( courseName: courseStructure.displayName, blockId: continueBlock?.id ?? "", @@ -225,14 +226,14 @@ extension CourseTab { return try await interactor.getLoadedCourseBlocks(courseID: courseID) } } - + @MainActor func getCourseBlocks(courseID: String, withProgress: Bool = true) async { guard let courseStart, courseStart < Date() else { return } - + isShowProgress = withProgress isShowRefresh = !withProgress - + async let structureTask = getCourseStructure(courseID: courseID) async let progressTask: CourseProgressDetails? = { do { @@ -246,7 +247,7 @@ extension CourseTab { return nil } }() - + do { guard let courseStructure = try await structureTask else { throw NSError( @@ -255,31 +256,31 @@ extension CourseTab { userInfo: [NSLocalizedDescriptionKey: "Course structure is nil"] ) } - + self.courseStructure = courseStructure courseHelper.courseStructure = courseStructure await courseHelper.refreshValue() update(from: courseHelper.value ?? .empty) - + // progress may still be downloading; assign when ready self.courseProgressDetails = await progressTask - + async let videosTask = interactor.getCourseVideoBlocks(fullStructure: courseStructure) async let assignmentsTask = interactor.getCourseAssignmentBlocks(fullStructure: courseStructure) - + courseVideosStructure = await videosTask courseAssignmentsStructure = await assignmentsTask updateAssignmentSections() - + if isInternetAvaliable { NotificationCenter.default.post(name: .getCourseDates, object: courseID) try? await getResumeBlock(courseID: courseID, courseStructure: courseStructure) } - + if expandedSections.isEmpty { initializeExpandedSections() } - + } catch { // Critical failure (no structure) — wipe everything debugLog("Failed to load course blocks: \(error.localizedDescription)") @@ -292,7 +293,7 @@ extension CourseTab { isShowProgress = false isShowRefresh = false } - + @MainActor func getCourseDeadlineInfo(courseID: String, withProgress: Bool = true) async { do { @@ -304,18 +305,18 @@ extension CourseTab { debugLog(error.localizedDescription) } } - + @MainActor func shiftDueDates(courseID: String, withProgress: Bool = true, screen: DatesStatusInfoScreen, type: String) async { isShowProgress = withProgress isShowRefresh = !withProgress - + do { try await interactor.shiftDueDates(courseID: courseID) NotificationCenter.default.post(name: .shiftCourseDates, object: courseID) isShowProgress = false isShowRefresh = false - + analytics.plsSuccessEvent( .plsShiftDatesSuccess, bivalue: .plsShiftDatesSuccess, @@ -324,7 +325,7 @@ extension CourseTab { type: type, success: true ) - + } catch let error { isShowProgress = false isShowRefresh = false @@ -343,19 +344,19 @@ extension CourseTab { } } } - + func update(downloadQuality: DownloadQuality) { storage.userSettings?.downloadQuality = downloadQuality userSettings = storage.userSettings courseHelper.videoQuality = downloadQuality courseHelper.refreshValue() } - + @MainActor func tryToRefreshCookies() async { try? await authInteractor.getCookies(force: false) } - + @MainActor private func getResumeBlock(courseID: String, courseStructure: CourseStructure) async throws { if let lastVisitedBlockID { @@ -374,7 +375,7 @@ extension CourseTab { } } } - + @MainActor func onDownloadViewTap(chapter: CourseChapter, state: DownloadViewState) async { let blocks = chapter.childs @@ -402,7 +403,7 @@ extension CourseTab { await download(state: state, blocks: blocks, sequentials: chapter.childs.filter({ $0.isDownloadable })) } - + func continueDownload() async { guard let blocks = waitingDownloads else { return @@ -415,7 +416,7 @@ extension CourseTab { } } } - + func trackSelectedTab( selection: CourseTab, courseId: String, @@ -438,7 +439,7 @@ extension CourseTab { analytics.courseOutlineHandoutsTabClicked(courseId: courseId, courseName: courseName) } } - + func trackVerticalClicked( courseId: String, courseName: String, @@ -451,7 +452,7 @@ extension CourseTab { blockName: vertical.displayName ) } - + func trackViewCertificateClicked(courseID: String) { analytics.trackCourseEvent( .courseViewCertificateClicked, @@ -459,7 +460,7 @@ extension CourseTab { courseID: courseID ) } - + func trackSequentialClicked(_ sequential: CourseSequential) { guard let course = courseStructure else { return } analytics.sequentialClicked( @@ -469,7 +470,7 @@ extension CourseTab { blockName: sequential.displayName ) } - + func trackSectionClicked(_ chapter: CourseChapter) { guard let course = courseStructure else { return } analytics.contentPageSectionClicked( @@ -479,7 +480,7 @@ extension CourseTab { blockName: chapter.displayName ) } - + func trackShowCompletedSubsectionClicked() { guard let course = courseStructure else { return } analytics.contentPageShowCompletedSubsectionClicked( @@ -542,9 +543,9 @@ extension CourseTab { func trackCourseHomeAssignmentClicked(blockId: String, blockName: String) { guard let course = courseStructure else { return } analytics.courseHomeAssignmentClicked(courseId: course.id, - courseName: course.displayName, - blockId: blockId, - blockName: blockName + courseName: course.displayName, + blockId: blockId, + blockName: blockName ) } @@ -557,7 +558,7 @@ extension CourseTab { blockName: sequential.displayName ) } - + func trackResumeCourseClicked(blockId: String) { guard let course = courseStructure else { return } analytics.resumeCourseClicked( @@ -566,7 +567,7 @@ extension CourseTab { blockId: blockId ) } - + func completeBlock( chapterID: String, sequentialID: String, @@ -582,14 +583,14 @@ extension CourseTab { .childs.firstIndex(where: { $0.id == sequentialID }) else { return } - + guard let verticalIndex = courseStructure? .childs[chapterIndex] .childs[sequentialIndex] .childs.firstIndex(where: { $0.id == verticalID }) else { return } - + guard let blockIndex = courseStructure? .childs[chapterIndex] .childs[sequentialIndex] @@ -597,20 +598,20 @@ extension CourseTab { .childs.firstIndex(where: { $0.id == blockID }) else { return } - + courseStructure? .childs[chapterIndex] .childs[sequentialIndex] .childs[verticalIndex] .childs[blockIndex].completion = 1 - + if let courseStructure { courseVideosStructure = await interactor.getCourseVideoBlocks(fullStructure: courseStructure) courseAssignmentsStructure = await interactor.getCourseAssignmentBlocks(fullStructure: courseStructure) updateAssignmentSections() } } - + func hasVideoForDowbloads() -> Bool { guard let courseVideosStructure = courseVideosStructure else { return false @@ -619,7 +620,7 @@ extension CourseTab { .flatMap { $0.childs } .contains(where: { $0.isDownloadable }) } - + func isAllDownloading() -> Bool { let totalCount = downloadableVerticals.count let downloadingCount = downloadableVerticals.filter { $0.state == .downloading }.count @@ -627,7 +628,7 @@ extension CourseTab { if finishedCount == totalCount { return false } return totalCount - finishedCount == downloadingCount } - + @MainActor func isAllDownloaded() -> Bool { guard let course = courseStructure else { return false } @@ -647,7 +648,7 @@ extension CourseTab { } return true } - + @MainActor func download(state: DownloadViewState, blocks: [CourseBlock], sequentials: [CourseSequential]) async { do { @@ -665,7 +666,7 @@ extension CourseTab { } } } - + private func presentNoInternetAlert(sequentials: [CourseSequential]) { router.presentView( transitionStyle: .coverVertical, @@ -680,7 +681,7 @@ extension CourseTab { completion: {} ) } - + private func presentWifiRequiredAlert(sequentials: [CourseSequential]) { router.presentView( transitionStyle: .coverVertical, @@ -695,7 +696,7 @@ extension CourseTab { completion: {} ) } - + @MainActor private func presentConfirmDownloadCellularAlert( blocks: [CourseBlock], @@ -729,7 +730,7 @@ extension CourseTab { completion: {} ) } - + private func presentStorageFullAlert(sequentials: [CourseSequential]) { router.presentView( transitionStyle: .coverVertical, @@ -745,7 +746,7 @@ extension CourseTab { completion: {} ) } - + @MainActor private func presentConfirmDownloadAlert( blocks: [CourseBlock], @@ -780,7 +781,7 @@ extension CourseTab { completion: {} ) } - + private func presentRemoveDownloadAlert(blocks: [CourseBlock], sequentials: [CourseSequential]) async { router.presentView( transitionStyle: .coverVertical, @@ -805,7 +806,7 @@ extension CourseTab { completion: {} ) } - + @MainActor func collectBlocks( chapter: CourseChapter, @@ -815,18 +816,18 @@ extension CourseTab { ) async -> [CourseBlock] { let sequentials = chapter.childs.filter { $0.id == blockId } guard !sequentials.isEmpty else { return [] } - + let blocks = sequentials.flatMap { $0.childs.flatMap { $0.childs } } .filter { $0.isDownloadable && (!videoOnly || $0.type == .video) } - + if state == .available, await isShowedAllowLargeDownloadAlert(blocks: blocks) { return [] } - + guard let sequential = chapter.childs.first(where: { $0.id == blockId }) else { return [] } - + if state == .available { analytics.bulkDownloadVideosSubsection( courseID: courseStructure?.id ?? "", @@ -841,10 +842,10 @@ extension CourseTab { videos: blocks.count ) } - + return blocks } - + @MainActor func isShowedAllowLargeDownloadAlert(blocks: [CourseBlock]) async -> Bool { waitingDownloads = nil @@ -869,13 +870,13 @@ extension CourseTab { } return false } - + @MainActor func downloadAll() async { guard let course = courseStructure else { return } var blocksToDownload: [CourseBlock] = [] var sequentialsToDownload: [CourseSequential] = [] - + for chapter in course.childs { for sequential in chapter.childs where sequential.isDownloadable { let blocks = downloadableBlocks(from: sequential) @@ -894,10 +895,10 @@ extension CourseTab { } } } - + if !blocksToDownload.isEmpty { let totalFileSize = blocksToDownload.reduce(0) { $0 + ($1.fileSize ?? 0) } - + if !connectivity.isInternetAvaliable { presentNoInternetAlert(sequentials: sequentialsToDownload) } else if connectivity.isMobileData { @@ -932,12 +933,12 @@ extension CourseTab { } } } - + @MainActor func isBlockDownloaded(_ block: CourseBlock) -> Bool { courseDownloadTasks.contains { $0.blockId == block.id && $0.state == .finished } } - + @MainActor func stopAllDownloads() async { do { @@ -947,7 +948,7 @@ extension CourseTab { errorMessage = CoreLocalization.Error.unknownError } } - + @MainActor func downloadableBlocks(from sequential: CourseSequential) -> [CourseBlock] { let verticals = sequential.childs @@ -956,7 +957,7 @@ extension CourseTab { .filter { $0.isDownloadable } return blocks } - + private func getFileSize(at url: URL) -> Int? { do { let fileAttributes = try FileManager.default.attributesOfItem(atPath: url.path) @@ -989,19 +990,19 @@ extension CourseTab { } return nil } - + private func isEnoughSpace(for fileSize: Int) -> Bool { if let freeSpace = manager.getFreeDiskSpace() { return freeSpace > Int(Double(fileSize) * 1.2) } return false } - + private func getUsedDiskSpace() -> Int? { do { let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String) if let totalSpace = attributes[.systemSize] as? Int64, - let freeSpace = attributes[.systemFreeSize] as? Int64 { + let freeSpace = attributes[.systemFreeSize] as? Int64 { return Int(totalSpace - freeSpace) } } catch { @@ -1009,7 +1010,7 @@ extension CourseTab { } return nil } - + // MARK: Larges Downloads @MainActor func removeBlock(_ block: CourseBlock) async { @@ -1039,7 +1040,7 @@ extension CourseTab { completion: {} ) } - + @MainActor func removeAllBlocks() async { let totalSize = courseDownloadTasks.reduce(0, { $0 + $1.actualSize }) @@ -1050,7 +1051,7 @@ extension CourseTab { } return false } - + router.presentView( transitionStyle: .coverVertical, view: DownloadActionView( @@ -1076,7 +1077,7 @@ extension CourseTab { completion: {} ) } - + private func update(from value: CourseDownloadValue) { downloadableVerticals = value.downloadableVerticals downloadAllButtonState = value.state @@ -1091,7 +1092,7 @@ extension CourseTab { private func initializeExpandedSections() { guard let courseStructure = courseStructure else { return } - + for chapter in courseStructure.childs { let progress = chapterProgress(for: chapter) let isNotCompleted = progress < 1.0 @@ -1125,10 +1126,10 @@ extension CourseTab { func chapterProgress(for chapter: CourseChapter) -> Double { guard !chapter.childs.isEmpty else { return 0.0 } - + let totalProgress = chapter.childs.reduce(0.0) { $0 + $1.completion } let averageProgress = totalProgress / Double(chapter.childs.count) - + return max(0.0, min(1.0, averageProgress)) } @@ -1166,7 +1167,7 @@ extension CourseTab { selector: #selector(handleShiftDueDates), name: .shiftCourseDates, object: nil ) - + completionPublisher .receive(on: DispatchQueue.main) .sink { [weak self] notification in @@ -1180,11 +1181,11 @@ extension CourseTab { } .store(in: &cancellables) } - + deinit { NotificationCenter.default.removeObserver(self) } - + func handleVideoTap(video: CourseBlock, chapter: CourseChapter?) { // Find indices for navigation using full course structure guard let chapterIndex = findChapterIndexInFullStructure(video: video), @@ -1193,7 +1194,7 @@ extension CourseTab { let courseStructure = courseStructure else { return } - + // Track video click analytics analytics.courseVideoClicked( courseId: courseStructure.id, @@ -1201,7 +1202,7 @@ extension CourseTab { blockId: video.id, blockName: video.displayName ) - + router.showCourseUnit( courseName: courseStructure.displayName, blockId: video.id, @@ -1214,10 +1215,10 @@ extension CourseTab { courseVideoStructure: courseStructure ) } - + private func findChapterIndexInFullStructure(video: CourseBlock) -> Int? { guard let courseStructure = courseStructure else { return nil } - + // Find the chapter that contains this video in the full structure return courseStructure.childs.firstIndex { fullChapter in fullChapter.childs.contains { sequential in @@ -1227,10 +1228,10 @@ extension CourseTab { } } } - + private func findSequentialIndexInFullStructure(video: CourseBlock) -> Int? { guard let courseStructure = courseStructure else { return nil } - + // Find the chapter and sequential that contains this video in the full structure for fullChapter in courseStructure.childs { if let sequentialIndex = fullChapter.childs.firstIndex(where: { sequential in @@ -1243,10 +1244,10 @@ extension CourseTab { } return nil } - + private func findVerticalIndexInFullStructure(video: CourseBlock) -> Int? { guard let courseStructure = courseStructure else { return nil } - + // Find the vertical that contains this video in the full structure for fullChapter in courseStructure.childs { for sequential in fullChapter.childs { @@ -1266,41 +1267,81 @@ extension CourseTab { let updatedStructure = updateBlockProgress(in: courseStructure, blockID: blockID, progress: progress) self.courseStructure = updatedStructure } - + if let courseStructure = courseStructure { let videoStructure = await interactor.getCourseVideoBlocks(fullStructure: courseStructure) self.courseVideosStructure = videoStructure self.courseAssignmentsStructure = await interactor.getCourseAssignmentBlocks(fullStructure: courseStructure) updateAssignmentSections() } - -// objectWillChange.send() } - + + @MainActor + public func refreshLocalVideoProgress() async { + guard !isRefreshingVideoProgress else { + return + } + + guard let courseStructure = courseStructure else { + return + } + + isRefreshingVideoProgress = true + + var updatedStructure = courseStructure + var videosProcessed = 0 + var videosUpdated = 0 + + for (chapterIndex, chapter) in courseStructure.childs.enumerated() { + for (sequentialIndex, sequential) in chapter.childs.enumerated() { + for (verticalIndex, vertical) in sequential.childs.enumerated() { + for (blockIndex, block) in vertical.childs.enumerated() where block.type == .video { + videosProcessed += 1 + + if let progress = await interactor.loadLocalVideoProgress(blockID: block.id) { + videosUpdated += 1 + updatedStructure + .childs[chapterIndex] + .childs[sequentialIndex] + .childs[verticalIndex] + .childs[blockIndex].localVideoProgress = progress + } + } + } + } + } + + self.courseStructure = updatedStructure + self.courseVideosStructure = nil + let newVideoStructure = await interactor.getCourseVideoBlocks(fullStructure: updatedStructure) + self.courseVideosStructure = newVideoStructure + self.courseAssignmentsStructure = await interactor.getCourseAssignmentBlocks(fullStructure: updatedStructure) + updateAssignmentSections() + isRefreshingVideoProgress = false + } + @MainActor func updateAssignmentProgress(blockID: String, progress: Double) async { if let courseStructure = courseStructure { let updatedStructure = updateBlockProgress(in: courseStructure, blockID: blockID, progress: progress) self.courseStructure = updatedStructure } - + if let courseStructure = courseStructure { let assignmentStructure = await interactor.getCourseAssignmentBlocks(fullStructure: courseStructure) self.courseAssignmentsStructure = assignmentStructure self.courseVideosStructure = await interactor.getCourseVideoBlocks(fullStructure: courseStructure) updateAssignmentSections() } - -// objectWillChange.send() } - + private func updateBlockProgress( in structure: CourseStructure, blockID: String, progress: Double ) -> CourseStructure { var updatedStructure = structure - + for (chapterIndex, chapter) in structure.childs.enumerated() { for (sequentialIndex, sequential) in chapter.childs.enumerated() { for (verticalIndex, vertical) in sequential.childs.enumerated() { @@ -1317,10 +1358,10 @@ extension CourseTab { } } } - + return updatedStructure } - + func courseProgress() -> CourseProgress? { guard let course = courseStructure else { return nil } let total = course.childs.count @@ -1328,19 +1369,19 @@ extension CourseTab { let completed = course.childs.filter { chapterProgress(for: $0) >= 1.0 }.count return CourseProgress(totalAssignmentsCount: total, assignmentsCompleted: completed) } - + func assignmentTypeProgress(for assignmentType: String) -> AssignmentProgressData? { guard let progressDetails = courseProgressDetails else { return nil } - + let subsectionsOfType = progressDetails.sectionScores.flatMap { $0.subsections } .filter { $0.assignmentType == assignmentType } - + guard !subsectionsOfType.isEmpty else { return nil } - + let totalPoints = subsectionsOfType.reduce(0) { $0 + $1.numPointsPossible } let earnedPoints = subsectionsOfType.reduce(0) { $0 + $1.numPointsEarned } let completed = subsectionsOfType.filter { $0.numPointsEarned >= $0.numPointsPossible }.count - + return AssignmentProgressData( completed: completed, total: subsectionsOfType.count, @@ -1348,26 +1389,26 @@ extension CourseTab { possiblePoints: totalPoints ) } - + func assignmentTypeWeight(for assignmentType: String) -> Double? { guard let progressDetails = courseProgressDetails else { return nil } - + return progressDetails.gradingPolicy.assignmentPolicies .first { $0.type == assignmentType }? .weight } - + func assignmentTypeLabel(for assignmentType: String) -> String? { guard let progressDetails = courseProgressDetails else { return nil } - + return progressDetails.gradingPolicy.assignmentPolicies .first { $0.type == assignmentType }? .type } - + func assignmentTypeColor(for assignmentType: String) -> String? { guard let progressDetails = courseProgressDetails else { return nil } - + if let index = progressDetails.gradingPolicy.assignmentPolicies .firstIndex(where: { $0.type == assignmentType }) { let colors = progressDetails.gradingPolicy.assignmentColors @@ -1375,10 +1416,10 @@ extension CourseTab { } return nil } - + func getSequentialShortLabel(for blockKey: String) -> String? { guard let courseStructure = courseAssignmentsStructure ?? courseStructure else { return nil } - + for chapter in courseStructure.childs { for sequential in chapter.childs { if sequential.blockId == blockKey || sequential.id == blockKey { @@ -1388,28 +1429,28 @@ extension CourseTab { } return nil } - + func getSequentialAssignmentStatus(for blockKey: String) -> AssignmentCardStatus? { guard let courseStructure = courseAssignmentsStructure ?? courseStructure else { return nil } - + for chapter in courseStructure.childs { for sequential in chapter.childs { if sequential.blockId == blockKey || sequential.id == blockKey { if sequential.completion >= 1.0 { return .completed } - + if let due = sequential.due, due < Date() { return .pastDue } - + return .incomplete } } } return nil } - + private func createUIModels(from subsections: [CourseProgressSubsection]) -> [CourseProgressSubsectionUI] { return subsections.map { subsection in let shortLabel = getSequentialShortLabel(for: subsection.blockKey) ?? "" @@ -1457,7 +1498,7 @@ extension CourseTab { assignmentSectionsData = [] return } - + let subsectionsByType = Dictionary( grouping: progressDetails.sectionScores.flatMap { $0.subsections } ) { subsection in @@ -1479,18 +1520,18 @@ extension CourseTab { subsections: uiSubsections ) } - + } - + func assignmentSections() -> [AssignmentSection] { return assignmentSectionsData } - + // MARK: - Assignment Deadline Methods - + func getAssignmentDeadline(for subsection: CourseProgressSubsection) -> CourseDateBlock? { guard let courseDeadlines = courseDeadlines else { return nil } - + // Trying to find deadline by Blockkey or other parameters return courseDeadlines.courseDateBlocks.first { dateBlock in // Binding by AssignmentType and name @@ -1498,55 +1539,55 @@ extension CourseTab { dateBlock.firstComponentBlockID.contains(subsection.blockKey) } } - + func getAssignmentStatus( - for subsection: CourseProgressSubsection + for subsection: CourseProgressSubsection ) -> AssignmentCardStatus { - // 1. No access - guard subsection.learnerHasAccess else { - return .notAvailable - } + // 1. No access + guard subsection.learnerHasAccess else { + return .notAvailable + } - // 2. Completed - if subsection.numPointsEarned >= subsection.numPointsPossible { - return .completed - } + // 2. Completed + if subsection.numPointsEarned >= subsection.numPointsPossible { + return .completed + } - // 3. Past due? - if isPastDue(subsection) { - return .pastDue - } + // 3. Past due? + if isPastDue(subsection) { + return .pastDue + } - // 4. All other cases - return .incomplete + // 4. All other cases + return .incomplete } // Helper function to check if past due: private func isPastDue( - _ subsection: CourseProgressSubsection + _ subsection: CourseProgressSubsection ) -> Bool { - guard - let structure = courseAssignmentsStructure ?? courseStructure - else { - return false - } - - // Flatten all sequentials into one array and find by key - let allSequentials = structure.childs.flatMap { $0.childs } - if let seq = allSequentials.first( - where: { $0.blockId == subsection.blockKey || $0.id == subsection.blockKey } - ), - let due = seq.due, - due < Date() { - return true - } + guard + let structure = courseAssignmentsStructure ?? courseStructure + else { + return false + } + + // Flatten all sequentials into one array and find by key + let allSequentials = structure.childs.flatMap { $0.childs } + if let seq = allSequentials.first( + where: { $0.blockId == subsection.blockKey || $0.id == subsection.blockKey } + ), + let due = seq.due, + due < Date() { + return true + } - return false + return false } - + func getDaysUntilDeadline(for subsection: CourseProgressSubsection) -> Int? { guard let courseStructure = courseAssignmentsStructure ?? courseStructure else { return nil } - + for chapter in courseStructure.childs { for sequential in chapter.childs { if sequential.blockId == subsection.blockKey || sequential.id == subsection.blockKey { @@ -1562,10 +1603,10 @@ extension CourseTab { } return nil } - + func getAssignmentDueDate(for subsection: CourseProgressSubsection) -> Date? { guard let courseStructure = courseAssignmentsStructure ?? courseStructure else { return nil } - + for chapter in courseStructure.childs { for sequential in chapter.childs { if sequential.blockId == subsection.blockKey || sequential.id == subsection.blockKey { @@ -1575,7 +1616,7 @@ extension CourseTab { } return nil } - + func clearShortLabel(_ text: String) -> String { let words = text.split(separator: " ") @@ -1591,14 +1632,14 @@ extension CourseTab { return leftShort + rightClean } - + private func computeStatusText( for subsection: CourseProgressSubsection, status: AssignmentCardStatus, shortLabel: String? ) -> String { let cleanShortLabel = clearShortLabel(shortLabel ?? "") - + switch status { case .completed: return CourseLocalization.AssignmentStatus @@ -1643,9 +1684,9 @@ extension CourseTab { func getAssignmentStatusText(for subsection: CourseProgressSubsection) -> String { let status = getAssignmentStatus(for: subsection) - + let shortLabel = clearShortLabel(subsection.shortLabel ?? "") - + switch status { case .completed: return CourseLocalization.AssignmentStatus @@ -1664,32 +1705,32 @@ extension CourseTab { } } } - + func getAssignmentSequenceName(for subsection: CourseProgressSubsection) -> String { // Trying to find Sequence Name from Course Structure guard let courseStructure = courseStructure else { return CourseLocalization.Assignment.unknownSequence } - + // Looking for a block in the structure of the course for chapter in courseStructure.childs { for sequential in chapter.childs { for vertical in sequential.childs where vertical.childs .contains(where: { $0.id == subsection.blockKey }) { - return sequential.displayName - } + return sequential.displayName + } } } - + return subsection.displayName } - + func navigateToAssignment(for subsection: CourseProgressSubsection) { guard let courseStructure = courseStructure else { return } for (chapterIndex, chapter) in courseStructure.childs.enumerated() { for (sequentialIndex, sequential) in chapter.childs.enumerated() - where sequential.id == subsection.blockKey { + where sequential.id == subsection.blockKey { guard let courseVertical = sequential.childs.first else { return } guard let firstBlock = courseVertical.childs.first else { router.showGatedContentError(url: courseVertical.webUrl) @@ -1744,7 +1785,7 @@ extension CourseContainerViewModel { } } } - + func resetDueDatesShiftedFlag() { dueDatesShifted = false } @@ -1753,7 +1794,7 @@ extension CourseContainerViewModel { public struct VerticalsDownloadState: Hashable, Sendable { public let vertical: CourseVertical public let state: DownloadViewState - + public var downloadableBlocks: [CourseBlock] { vertical.childs.filter { $0.isDownloadable && $0.type == .video } } diff --git a/Course/Course/Presentation/Content/CourseContentView.swift b/Course/Course/Presentation/Content/CourseContentView.swift index 131bbc64d..11e9a6c25 100644 --- a/Course/Course/Presentation/Content/CourseContentView.swift +++ b/Course/Course/Presentation/Content/CourseContentView.swift @@ -223,6 +223,11 @@ public struct CourseContentView: View { await viewModel.updateVideoProgress(blockID: blockID, progress: progress) } } + .onAppear { + Task { + await viewModel.refreshLocalVideoProgress() + } + } case .assignments: AssignmentsContentView( assignmentContentData: assignmentContentData, diff --git a/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift b/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift index 392b88541..528d3c141 100644 --- a/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift +++ b/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift @@ -174,11 +174,13 @@ public struct CourseOutlineAndProgressView: View { .opacity(viewModelProgress.isLoading || viewModelContainer.isShowProgress ? 0 : 1) } .onAppear { - if viewModelProgress.courseProgress == nil { - Task { + Task { + if viewModelProgress.courseProgress == nil { await viewModelProgress.getCourseProgress(courseID: courseID) await viewModelContainer .getCourseBlocks(courseID: courseID, withProgress: false) + } else { + await viewModelContainer.refreshLocalVideoProgress() } } } diff --git a/Course/Course/Presentation/Video/PlayerServiceProtocol.swift b/Course/Course/Presentation/Video/PlayerServiceProtocol.swift index e632e3ebc..4768f1ecf 100644 --- a/Course/Course/Presentation/Video/PlayerServiceProtocol.swift +++ b/Course/Course/Presentation/Video/PlayerServiceProtocol.swift @@ -72,7 +72,7 @@ public final class PlayerService: PlayerServiceProtocol { public func updateVideoProgress(progress: Double) async { await interactor.updateLocalVideoProgress(blockID: blockID, progress: progress) - + NotificationCenter.default.post( name: .onVideoProgressUpdated, object: nil, diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 6da157ace..3edbac826 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -360,6 +360,7 @@ class ScreenAssembly: Assembly { courseHelper: r.resolve(CourseDownloadHelperProtocol.self)! ) } + .inObjectScope(.weak) container.register( CourseDownloadHelperProtocol.self ) { @MainActor r in From 6e61743425f2fae1069ad982a6e58450357dc798 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 8 Jan 2026 13:56:37 +0200 Subject: [PATCH 28/51] fix: video completion --- .../CourseCarouselView/CourseVideoCarouselSlideView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift index 0bdb40064..1d547c883 100644 --- a/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift +++ b/Course/Course/Presentation/Subviews/CourseCarouselView/CourseVideoCarouselSlideView.swift @@ -84,7 +84,7 @@ struct CourseVideoCarouselSlideView: View { guard let chapter = courseChapter else { return nil } let videos = getAllVideos(from: chapter) if let partial = videos.first(where: { - $0.localVideoProgress > 0 && $0.localVideoProgress < 1 + $0.localVideoProgress > 0 && $0.localVideoProgress < 1 && $0.completion < 1 }) { return partial } else { From 481a13dfd3a18d98be20b1d18e08871dd6937077 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 8 Jan 2026 18:39:46 +0200 Subject: [PATCH 29/51] feat: updates to Discussions fixes --- .../Comments/Thread/ThreadViewModel.swift | 21 +++++++++---------- .../Presentation/Posts/PostsViewModel.swift | 15 +++++++------ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index 9939d5375..646197f3e 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -13,10 +13,9 @@ import Core public final class ThreadViewModel: BaseResponsesViewModel { var scrollTrigger: Bool = false - - internal let threadStateSubject = CurrentValueSubject(nil) - private var cancellable: AnyCancellable? - private let postStateSubject: CurrentValueSubject + + @ObservationIgnored internal let threadStateSubject = CurrentValueSubject(nil) + @ObservationIgnored private let postStateSubject: CurrentValueSubject public var isBlackedOut: Bool = false private let analytics: DiscussionAnalytics? @@ -30,13 +29,12 @@ public final class ThreadViewModel: BaseResponsesViewModel { ) { self.postStateSubject = postStateSubject self.analytics = analytics - + super.init(interactor: interactor, router: router, config: config, storage: storage, analytics: analytics) - - cancellable = threadStateSubject - .receive(on: RunLoop.main) - .sink(receiveValue: { [weak self] state in - guard let self, let state else { return } + + Task { + for await state in threadStateSubject.values { + guard let state = state else { continue } switch state { case let .voted(id, voted, votesCount): self.updateThreadLikeState(id: id, voted: voted, votesCount: votesCount) @@ -46,7 +44,8 @@ public final class ThreadViewModel: BaseResponsesViewModel { self.updateThreadPostsCountState(id: id) self.sendPostRepliesCountState() } - }) + } + } } func generateComments(comments: [UserComment], thread: UserThread) -> Post { diff --git a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift index f36cecdaf..0e21c6a0d 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift @@ -84,8 +84,7 @@ public final class PostsViewModel { private let router: DiscussionRouter private let config: ConfigProtocol private let storage: CoreStorage - internal let postStateSubject = CurrentValueSubject(nil) - private var cancellable: AnyCancellable? + @ObservationIgnored internal let postStateSubject = CurrentValueSubject(nil) public init( interactor: DiscussionInteractorProtocol, @@ -97,11 +96,10 @@ public final class PostsViewModel { self.router = router self.config = config self.storage = storage - - cancellable = postStateSubject - .receive(on: RunLoop.main) - .sink(receiveValue: { [weak self] state in - guard let self, let state else { return } + + Task { + for await state in postStateSubject.values { + guard let state = state else { continue } switch state { case let .followed(id, followed): self.updatePostFollowedState(id: id, followed: followed) @@ -114,7 +112,8 @@ public final class PostsViewModel { case let .reported(id, reported): self.updatePostReportedState(id: id, reported: reported) } - }) + } + } } public func resetPosts() { From d246e71c4dccb9c85c4310f451388aac9b92aa77 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 8 Jan 2026 20:20:48 +0200 Subject: [PATCH 30/51] fix: updates to Course module --- .../Container/CourseContainerViewModel.swift | 12 +++---- .../Dates/CourseDatesViewModel.swift | 11 +++--- .../Downloads/DownloadsViewModel.swift | 7 ++-- .../Handouts/HandoutsViewModel.swift | 13 +++---- .../ CourseOutlineAndProgressViewModel.swift | 21 +++++------ .../CourseVerticalViewModel.swift | 13 +++---- .../Progress/CourseProgressViewModel.swift | 13 +++---- .../CourseVideoDownloadBarViewModel.swift | 36 +++++++++---------- .../Unit/CourseUnitViewModel.swift | 10 +++--- .../Video/VideoPlayerViewModel.swift | 12 +++---- 10 files changed, 66 insertions(+), 82 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index a23723f21..ede4a19fe 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -77,7 +77,7 @@ extension CourseTab { var courseVideosStructure: CourseStructure? var courseAssignmentsStructure: CourseStructure? var courseProgressDetails: CourseProgressDetails? - var showError: Bool = false +// var showError: Bool = false var sequentialsDownloadState: [String: DownloadViewState] = [:] private(set) var downloadableVerticals: Set = [] var continueWith: ContinueWith? @@ -99,12 +99,10 @@ extension CourseTab { let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) - var errorMessage: String? { - didSet { - withAnimation { - showError = errorMessage != nil - } - } + var errorMessage: String? + + var showError: Bool { + errorMessage != nil } let router: CourseRouter diff --git a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift index 2fe9188f5..c2473700c 100644 --- a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift +++ b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift @@ -23,17 +23,14 @@ public class CourseDatesViewModel { } var isShowProgress = true - var showError: Bool = false var courseDates: CourseDates? var isOn: Bool = false var eventState: EventState? - var errorMessage: String? { - didSet { - withAnimation { - showError = errorMessage != nil - } - } + var errorMessage: String? + + var showError: Bool { + errorMessage != nil } private let interactor: CourseInteractorProtocol diff --git a/Course/Course/Presentation/Downloads/DownloadsViewModel.swift b/Course/Course/Presentation/Downloads/DownloadsViewModel.swift index 92b5f6c5f..8410fcfba 100644 --- a/Course/Course/Presentation/Downloads/DownloadsViewModel.swift +++ b/Course/Course/Presentation/Downloads/DownloadsViewModel.swift @@ -67,8 +67,11 @@ final class DownloadsViewModel { .store(in: &cancellables) helper.progressPublisher() .sink {[weak self] task in - if let firstIndex = self?.downloads.firstIndex(where: { $0.id == task.id }) { - self?.downloads[firstIndex].progress = task.progress + guard let self = self else { return } + if let firstIndex = self.downloads.firstIndex(where: { $0.id == task.id }) { + var updatedDownloads = self.downloads + updatedDownloads[firstIndex].progress = task.progress + self.downloads = updatedDownloads } } .store(in: &cancellables) diff --git a/Course/Course/Presentation/Handouts/HandoutsViewModel.swift b/Course/Course/Presentation/Handouts/HandoutsViewModel.swift index 16467ba35..140867b50 100644 --- a/Course/Course/Presentation/Handouts/HandoutsViewModel.swift +++ b/Course/Course/Presentation/Handouts/HandoutsViewModel.swift @@ -14,16 +14,13 @@ import SwiftUI public final class HandoutsViewModel { private(set) var isShowProgress = false - var showError: Bool = false var handouts: String? var updates: [CourseUpdate] = [] - - var errorMessage: String? { - didSet { - withAnimation { - showError = errorMessage != nil - } - } + + var errorMessage: String? + + var showError: Bool { + errorMessage != nil } private let interactor: CourseInteractorProtocol diff --git a/Course/Course/Presentation/NewOutlIineAndProgress/ CourseOutlineAndProgressViewModel.swift b/Course/Course/Presentation/NewOutlIineAndProgress/ CourseOutlineAndProgressViewModel.swift index fe2ba7b6b..6c6c3dc9b 100644 --- a/Course/Course/Presentation/NewOutlIineAndProgress/ CourseOutlineAndProgressViewModel.swift +++ b/Course/Course/Presentation/NewOutlIineAndProgress/ CourseOutlineAndProgressViewModel.swift @@ -11,7 +11,6 @@ public class CourseOutlineAndProgressViewModel { // MARK: - Variables public var courseProgress: CourseProgressDetails? - public var showError: Bool = false public var selection: Int var userSettings: UserSettings? var isInternetAvaliable: Bool = true @@ -21,29 +20,27 @@ public class CourseOutlineAndProgressViewModel { let connectivity: ConnectivityProtocol let interactor: CourseInteractorProtocol let config: ConfigProtocol - + let isActive: Bool? let courseStart: Date? let courseEnd: Date? let enrollmentStart: Date? let enrollmentEnd: Date? let lastVisitedBlockID: String? - + var courseDownloadTasks: [DownloadDataTask] = [] private(set) var waitingDownloads: [CourseBlock]? - + private let authInteractor: AuthInteractorProtocol private(set) var storage: CourseStorage - + private let cellularFileSizeLimit: Int = 100 * 1024 * 1024 var courseHelper: CourseDownloadHelperProtocol - - public var errorMessage: String? { - didSet { - withAnimation { - showError = errorMessage != nil - } - } + + public var errorMessage: String? + + public var showError: Bool { + errorMessage != nil } // MARK: - Init diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift index 036be081c..4c193a769 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift @@ -15,17 +15,14 @@ public final class CourseVerticalViewModel: @unchecked Sendable { let analytics: CourseAnalytics let connectivity: ConnectivityProtocol var verticals: [CourseVertical] - var showError: Bool = false let chapters: [CourseChapter] let chapterIndex: Int let sequentialIndex: Int - - var errorMessage: String? { - didSet { - withAnimation { - showError = errorMessage != nil - } - } + + var errorMessage: String? + + var showError: Bool { + errorMessage != nil } public init( diff --git a/Course/Course/Presentation/Progress/CourseProgressViewModel.swift b/Course/Course/Presentation/Progress/CourseProgressViewModel.swift index a41bad855..f7ace021e 100644 --- a/Course/Course/Presentation/Progress/CourseProgressViewModel.swift +++ b/Course/Course/Presentation/Progress/CourseProgressViewModel.swift @@ -18,20 +18,17 @@ public class CourseProgressViewModel { var assignmentProgressData: [String: AssignmentProgressData] = [:] var isLoading: Bool = false var isShowRefresh = false - var showError: Bool = false - + let router: CourseRouter let analytics: CourseAnalytics let connectivity: ConnectivityProtocol let interactor: CourseInteractorProtocol var courseStructure: CourseStructure? - public var errorMessage: String? { - didSet { - withAnimation { - showError = errorMessage != nil - } - } + public var errorMessage: String? + + public var showError: Bool { + errorMessage != nil } public init( diff --git a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift index f4abfeaa2..7d105dd9b 100644 --- a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift +++ b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift @@ -44,29 +44,27 @@ final class CourseVideoDownloadBarViewModel { /// total progress of downloading video files var progress: Double = 0 - var downloadableVerticals: Set = [] { - didSet { - let downloading = downloadableVerticals.filter { $0.state == .downloading } - downloadingVideos = downloading.flatMap { $0.downloadableBlocks }.count - - let finished = downloadableVerticals.filter { $0.state == .finished } - totalFinishedVideos = finished.flatMap { $0.downloadableBlocks }.count - - let inProgress = downloadableVerticals.filter { $0.state != .finished } - remainingVideos = inProgress.flatMap { $0.downloadableBlocks }.count - - let totalFinishedCount = finished.count - isAllVideosDownloaded = totalFinishedCount == downloadableVerticals.count - } - } + var downloadableVerticals: Set = [] - var isAllVideosDownloaded: Bool = false + var downloadingVideos: Int { + let downloading = downloadableVerticals.filter { $0.state == .downloading } + return downloading.flatMap { $0.downloadableBlocks }.count + } - var remainingVideos: Int = 0 + var totalFinishedVideos: Int { + let finished = downloadableVerticals.filter { $0.state == .finished } + return finished.flatMap { $0.downloadableBlocks }.count + } - var downloadingVideos: Int = 0 + var remainingVideos: Int { + let inProgress = downloadableVerticals.filter { $0.state != .finished } + return inProgress.flatMap { $0.downloadableBlocks }.count + } - var totalFinishedVideos: Int = 0 + var isAllVideosDownloaded: Bool { + let finished = downloadableVerticals.filter { $0.state == .finished } + return finished.count == downloadableVerticals.count && !downloadableVerticals.isEmpty + } var totalSize: String? diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index 411690e77..0e618f6b3 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -135,11 +135,11 @@ public struct VerticalData: Equatable { var index: Int = 0 var previousLesson: String = "" var nextLesson: String = "" - var showError: Bool = false - var errorMessage: String? { - didSet { - showError = errorMessage != nil - } + + var errorMessage: String? + + var showError: Bool { + errorMessage != nil } public var allVideosForNavigation: [CourseBlock] = [] diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index 3c4fafbbd..b5ee6082c 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -26,13 +26,13 @@ public class VideoPlayerViewModel { var languages: [SubtitleUrl] var items: [PickerItem] = [] var selectedLanguage: String? - - var showError: Bool = false - var errorMessage: String? { - didSet { - showError = errorMessage != nil - } + + var errorMessage: String? + + var showError: Bool { + errorMessage != nil } + var isPlayingInPip: Bool { playerHolder.isPlayingInPip } From 07b5811e5af7b8eb8a3a786a1bf51349c769af99 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 3 Feb 2026 06:01:42 +0200 Subject: [PATCH 31/51] fix: minimum deploy target --- AppDates/AppDates.xcodeproj/project.pbxproj | 32 +++++++++---------- .../Authorization.xcodeproj/project.pbxproj | 16 +++++----- Core/Core.xcodeproj/project.pbxproj | 16 +++++----- Course/Course.xcodeproj/project.pbxproj | 16 +++++----- .../Container/CourseContainerViewModel.swift | 9 +----- Dashboard/Dashboard.xcodeproj/project.pbxproj | 16 +++++----- Discovery/Discovery.xcodeproj/project.pbxproj | 16 +++++----- .../Discussion.xcodeproj/project.pbxproj | 16 +++++----- Downloads/Downloads.xcodeproj/project.pbxproj | 16 +++++----- OpenEdX.xcodeproj/project.pbxproj | 12 +++---- Profile/Profile.xcodeproj/project.pbxproj | 16 +++++----- Theme/Theme.xcodeproj/project.pbxproj | 16 +++++----- 12 files changed, 95 insertions(+), 102 deletions(-) diff --git a/AppDates/AppDates.xcodeproj/project.pbxproj b/AppDates/AppDates.xcodeproj/project.pbxproj index 0438af748..a84850dda 100644 --- a/AppDates/AppDates.xcodeproj/project.pbxproj +++ b/AppDates/AppDates.xcodeproj/project.pbxproj @@ -554,7 +554,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -593,7 +593,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -747,7 +747,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = stepanok.com.AppDatesTests; @@ -771,7 +771,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = stepanok.com.AppDatesTests; @@ -870,7 +870,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -900,7 +900,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = stepanok.com.AppDatesTests; @@ -999,7 +999,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1029,7 +1029,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = stepanok.com.AppDatesTests; @@ -1128,7 +1128,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1158,7 +1158,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = stepanok.com.AppDatesTests; @@ -1250,7 +1250,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1279,7 +1279,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = stepanok.com.AppDatesTests; @@ -1371,7 +1371,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1400,7 +1400,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = stepanok.com.AppDatesTests; @@ -1492,7 +1492,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1521,7 +1521,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = stepanok.com.AppDatesTests; diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index ff13fc540..450229af7 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -732,7 +732,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -842,7 +842,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -860,7 +860,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -878,7 +878,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -896,7 +896,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -914,7 +914,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -932,7 +932,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -950,7 +950,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index a5275c669..8f2a090c5 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -1443,7 +1443,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1562,7 +1562,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1584,7 +1584,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1607,7 +1607,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1630,7 +1630,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1653,7 +1653,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1675,7 +1675,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1697,7 +1697,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index 9ec7cfb3b..c49ff05de 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -1285,7 +1285,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1307,7 +1307,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1329,7 +1329,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1351,7 +1351,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1373,7 +1373,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1395,7 +1395,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -2117,7 +2117,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -2235,7 +2235,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 2e34ccdf9..c41bec882 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -1418,14 +1418,7 @@ extension CourseTab { return index < colors.count ? colors[index] : nil } - let colors = progressDetails.gradingPolicy.assignmentColors - - guard !colors.isEmpty else { return nil } - - let colorIndex = index % colors.count - let hexColor = colors[colorIndex] - - return hexColor + return nil } func getSequentialShortLabel(for blockKey: String) -> String? { diff --git a/Dashboard/Dashboard.xcodeproj/project.pbxproj b/Dashboard/Dashboard.xcodeproj/project.pbxproj index c2c27071a..d3bf95401 100644 --- a/Dashboard/Dashboard.xcodeproj/project.pbxproj +++ b/Dashboard/Dashboard.xcodeproj/project.pbxproj @@ -559,7 +559,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -580,7 +580,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -601,7 +601,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -622,7 +622,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -643,7 +643,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -664,7 +664,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -784,7 +784,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -897,7 +897,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; diff --git a/Discovery/Discovery.xcodeproj/project.pbxproj b/Discovery/Discovery.xcodeproj/project.pbxproj index c0eba75c9..e546e4ec8 100644 --- a/Discovery/Discovery.xcodeproj/project.pbxproj +++ b/Discovery/Discovery.xcodeproj/project.pbxproj @@ -635,7 +635,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -656,7 +656,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -677,7 +677,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -698,7 +698,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -719,7 +719,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -740,7 +740,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -861,7 +861,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -975,7 +975,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; diff --git a/Discussion/Discussion.xcodeproj/project.pbxproj b/Discussion/Discussion.xcodeproj/project.pbxproj index 017fa5f48..5c70a31ff 100644 --- a/Discussion/Discussion.xcodeproj/project.pbxproj +++ b/Discussion/Discussion.xcodeproj/project.pbxproj @@ -1405,7 +1405,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.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 = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1447,7 +1447,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1468,7 +1468,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1489,7 +1489,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1510,7 +1510,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1630,7 +1630,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1743,7 +1743,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; diff --git a/Downloads/Downloads.xcodeproj/project.pbxproj b/Downloads/Downloads.xcodeproj/project.pbxproj index 067695a29..5706ba484 100644 --- a/Downloads/Downloads.xcodeproj/project.pbxproj +++ b/Downloads/Downloads.xcodeproj/project.pbxproj @@ -755,7 +755,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DownloadsTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -777,7 +777,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DownloadsTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -905,7 +905,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DownloadsTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1033,7 +1033,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DownloadsTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1161,7 +1161,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DownloadsTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1281,7 +1281,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DownloadsTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1401,7 +1401,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DownloadsTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1521,7 +1521,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DownloadsTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 00d02a2fb..9f1049c4b 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -723,7 +723,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -815,7 +815,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -913,7 +913,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1005,7 +1005,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1157,7 +1157,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1195,7 +1195,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index 1353b12d3..3ecb7eb88 100644 --- a/Profile/Profile.xcodeproj/project.pbxproj +++ b/Profile/Profile.xcodeproj/project.pbxproj @@ -1343,7 +1343,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1364,7 +1364,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1385,7 +1385,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1406,7 +1406,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1427,7 +1427,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1448,7 +1448,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1567,7 +1567,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1679,7 +1679,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; diff --git a/Theme/Theme.xcodeproj/project.pbxproj b/Theme/Theme.xcodeproj/project.pbxproj index 1fc77b799..5bee7a33e 100644 --- a/Theme/Theme.xcodeproj/project.pbxproj +++ b/Theme/Theme.xcodeproj/project.pbxproj @@ -509,7 +509,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -602,7 +602,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -700,7 +700,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -793,7 +793,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -891,7 +891,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -984,7 +984,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.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 = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1175,7 +1175,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From 83ae576b6c9ef63d0403a73654060919650c5d22 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 4 Feb 2026 07:26:29 +0200 Subject: [PATCH 32/51] fix: tests update --- Core/Core/Configuration/Combine/Debounce.swift | 8 +++++--- .../PrimaryCourseDashboardViewModel.swift | 3 +-- .../NativeDiscovery/SearchViewModel.swift | 2 +- .../Presentation/SearchViewModelTests.swift | 16 ++++++---------- .../DiscussionSearchTopicsView.swift | 3 +-- .../DiscussionSearchTopicsViewModelTests.swift | 8 ++++---- .../DiscussionTopicsViewModelTests.swift | 4 ++-- WhatsNew/WhatsNew.xcodeproj/project.pbxproj | 16 ++++++++-------- 8 files changed, 28 insertions(+), 32 deletions(-) diff --git a/Core/Core/Configuration/Combine/Debounce.swift b/Core/Core/Configuration/Combine/Debounce.swift index c14510842..2de271f0a 100644 --- a/Core/Core/Configuration/Combine/Debounce.swift +++ b/Core/Core/Configuration/Combine/Debounce.swift @@ -11,21 +11,23 @@ import Combine public struct Debounce { public let scheduler: S public let dueTime: S.SchedulerTimeType.Stride + public let dueTimeInMilliseconds: Int - public init(scheduler: S, dueTime: S.SchedulerTimeType.Stride) { + public init(scheduler: S, dueTime: S.SchedulerTimeType.Stride, dueTimeInMilliseconds: Int) { self.scheduler = scheduler self.dueTime = dueTime + self.dueTimeInMilliseconds = dueTimeInMilliseconds } } public extension Debounce where S == RunLoop { static var searchDebounce: Debounce { - Debounce(scheduler: RunLoop.main, dueTime: .milliseconds(800)) + Debounce(scheduler: RunLoop.main, dueTime: .milliseconds(800), dueTimeInMilliseconds: 800) } } public extension Debounce where S == ImmediateScheduler { static var test: Debounce { - Debounce(scheduler: ImmediateScheduler.shared, dueTime: .zero) + Debounce(scheduler: ImmediateScheduler.shared, dueTime: .zero, dueTimeInMilliseconds: 0) } } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index 140ea058a..43d70ac46 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -35,7 +35,7 @@ public class PrimaryCourseDashboardViewModel { let config: ConfigProtocol var storage: CoreStorage let router: DashboardRouter - @ObservationIgnored private var observers: [NSObjectProtocol] = [] + @ObservationIgnored nonisolated(unsafe) private var observers: [NSObjectProtocol] = [] private let ipadPageSize = 7 private let iphonePageSize = 5 @@ -152,7 +152,6 @@ public class PrimaryCourseDashboardViewModel { analytics.dashboardCourseClicked(courseID: courseID, courseName: courseName) } - @MainActor deinit { observers.forEach { NotificationCenter.default.removeObserver($0) } } diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift index 5484f2ec9..86865dc64 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift @@ -66,7 +66,7 @@ import Combine searchTask?.cancel() searchTask = Task { @MainActor in - try? await Task.sleep(for: .milliseconds(500)) + try? await Task.sleep(for: .milliseconds(debounce.dueTimeInMilliseconds)) guard !Task.isCancelled else { return } diff --git a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift index f8d805f3c..14c506122 100644 --- a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift @@ -73,9 +73,8 @@ final class SearchViewModelTests: XCTestCase { viewModel.searchText = "Test" - // Wait for debounce + next event loop iteration - try await Task.sleep(nanoseconds: UInt64(0.5 * Double(NSEC_PER_SEC))) - await Task.yield() + // Wait for search to complete + try await Task.sleep(for: .milliseconds(10)) XCTAssertTrue(interactor.searchCallCount > 0) XCTAssertTrue(analytics.discoveryCoursesSearchCallCount > 0) @@ -128,10 +127,8 @@ final class SearchViewModelTests: XCTestCase { viewModel.searchText = "Test" - // Wait for debounce + next event loop iteration - try await Task.sleep(nanoseconds: UInt64(0.5 * Double(NSEC_PER_SEC))) - await Task.yield() - + // Wait for search to complete + try await Task.sleep(for: .milliseconds(10)) XCTAssertEqual(interactor.searchCallCount, 1) @@ -160,9 +157,8 @@ final class SearchViewModelTests: XCTestCase { viewModel.searchText = "Test" - // Wait for debounce + next event loop iteration - try await Task.sleep(nanoseconds: UInt64(0.5 * Double(NSEC_PER_SEC))) - await Task.yield() + // Wait for search to complete + try await Task.sleep(for: .milliseconds(10)) XCTAssertEqual(interactor.searchCallCount, 1) diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift index 5f9edbfa6..ff09a8365 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift @@ -198,8 +198,7 @@ struct DiscussionSearchTopicsView_Previews: PreviewProvider { courseID: "123", interactor: DiscussionInteractor.mock, storage: CoreStorageMock(), - router: DiscussionRouterMock(), - debounce: .searchDebounce + router: DiscussionRouterPreviewMock() ) DiscussionSearchTopicsView(viewModel: vm) diff --git a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift index 68a582509..40e96d19b 100644 --- a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift @@ -52,7 +52,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { interactor: interactor, storage: storage, router: router, - debounce: .test) + debounceInterval: 0.1) viewModel.searchText = "Test" @@ -79,7 +79,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { interactor: interactor, storage: storage, router: router, - debounce: .test) + debounceInterval: 0.1) viewModel.searchText = "Test" @@ -105,7 +105,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { interactor: interactor, storage: storage, router: router, - debounce: .test) + debounceInterval: 0.1) viewModel.searchText = "Test" @@ -129,7 +129,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { interactor: interactor, storage: storage, router: router, - debounce: .test) + debounceInterval: 0.1) viewModel.searchText = "" diff --git a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift index 04f36babe..a93b988de 100644 --- a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift @@ -88,7 +88,7 @@ final class DiscussionTopicsViewModelTests: XCTestCase { XCTAssertEqual(interactor.getTopicsCallCount, 1) XCTAssertNil(viewModel.topics) - XCTAssertNil(viewModel.discussionTopics) + XCTAssertTrue(viewModel.discussionTopics.isEmpty) XCTAssertFalse(viewModel.isShowProgress) XCTAssertFalse(viewModel.isShowRefresh) } @@ -114,7 +114,7 @@ final class DiscussionTopicsViewModelTests: XCTestCase { XCTAssertEqual(interactor.getTopicsCallCount, 1) XCTAssertNil(viewModel.topics) - XCTAssertNil(viewModel.discussionTopics) + XCTAssertTrue(viewModel.discussionTopics.isEmpty) XCTAssertFalse(viewModel.isShowProgress) XCTAssertFalse(viewModel.isShowRefresh) } diff --git a/WhatsNew/WhatsNew.xcodeproj/project.pbxproj b/WhatsNew/WhatsNew.xcodeproj/project.pbxproj index 05bd5ced7..ce86d51b4 100644 --- a/WhatsNew/WhatsNew.xcodeproj/project.pbxproj +++ b/WhatsNew/WhatsNew.xcodeproj/project.pbxproj @@ -701,7 +701,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -722,7 +722,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -848,7 +848,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -974,7 +974,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1100,7 +1100,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1218,7 +1218,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1336,7 +1336,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1454,7 +1454,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; From 19ccc7e2a7464f4bd7e1f9e04bfd3563a43754ce Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 4 Feb 2026 07:48:08 +0200 Subject: [PATCH 33/51] fix: unit tests failure --- Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift b/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift index fa58b96be..223f97e5a 100644 --- a/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift +++ b/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift @@ -7,7 +7,7 @@ import SwiftUI final class KeyboardStateObserver { private(set) var keyboardState: KeyboardState = .default - private var observers: [NSObjectProtocol] = [] + nonisolated(unsafe) private var observers: [NSObjectProtocol] = [] init() { let notificationCenter = NotificationCenter.default @@ -60,7 +60,6 @@ final class KeyboardStateObserver { keyboardState = newState } - @MainActor deinit { observers.forEach { NotificationCenter.default.removeObserver($0) } } From 72753e01def90728a282a1632a1b168103d23d9c Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 4 Feb 2026 08:28:40 +0200 Subject: [PATCH 34/51] fix: KeyboardStateObserver --- .../AvoidingHelpers/State/KeyboardState.swift | 33 +++++++++++++++++++ .../State/KeyboardStateObserver.swift | 33 ++++++++++++------- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/Core/Core/AvoidingHelpers/State/KeyboardState.swift b/Core/Core/AvoidingHelpers/State/KeyboardState.swift index 338b90283..cf1f334b0 100644 --- a/Core/Core/AvoidingHelpers/State/KeyboardState.swift +++ b/Core/Core/AvoidingHelpers/State/KeyboardState.swift @@ -3,6 +3,16 @@ import SwiftUI import UIKit +struct NotificationData: @unchecked Sendable { + let name: Notification.Name + let userInfo: [AnyHashable: Any]? + + init(from notification: Notification) { + self.name = notification.name + self.userInfo = notification.userInfo + } +} + public struct KeyboardState: Sendable, Equatable { public let animationDuration: TimeInterval @@ -32,6 +42,7 @@ extension KeyboardState { frame: .zero ) + @MainActor static func from(notification: Notification) -> KeyboardState? { return from( notification: notification, @@ -39,6 +50,7 @@ extension KeyboardState { ) } + @MainActor static func from( notification: Notification, screen: UIScreen @@ -60,6 +72,26 @@ extension KeyboardState { frame: frame ) } + + @MainActor + static func from(notificationData: NotificationData) -> KeyboardState? { + guard + expectedNotificationNames.contains(notificationData.name), + let userInfo = notificationData.userInfo else { + return nil + } + + let animationDuration = Self.animationDuration(from: userInfo) + let animationCurve = Self.animationCurve(from: userInfo) + + let frame = Self.keyboardFrame(from: userInfo, screen: .main) + + return KeyboardState( + animationDuration: animationDuration, + animationCurve: animationCurve, + frame: frame + ) + } private static var expectedNotificationNames: [Notification.Name] { [ @@ -86,6 +118,7 @@ extension KeyboardState { return curveValue } + @MainActor private static func keyboardFrame( from userInfo: [AnyHashable: Any], screen: UIScreen diff --git a/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift b/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift index 223f97e5a..949e700ae 100644 --- a/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift +++ b/Core/Core/AvoidingHelpers/State/KeyboardStateObserver.swift @@ -19,35 +19,46 @@ final class KeyboardStateObserver { queue: .main ) { [weak self] notification in guard let self = self else { return } - - if let state = KeyboardState.from(notification: notification) { - self.updateState(state) + + let notificationData = NotificationData(from: notification) + + Task { @MainActor in + if let state = KeyboardState.from(notificationData: notificationData) { + self.updateState(state) + } } } - // Observe keyboard will change frame let changeObserver = notificationCenter.addObserver( forName: UIResponder.keyboardWillChangeFrameNotification, object: nil, queue: .main ) { [weak self] notification in guard let self = self else { return } - // Extract data from notification synchronously on main queue - if let state = KeyboardState.from(notification: notification) { - self.updateState(state) + + // Extract notification data in nonisolated context + let notificationData = NotificationData(from: notification) + + Task { @MainActor in + if let state = KeyboardState.from(notificationData: notificationData) { + self.updateState(state) + } } } - // Observe keyboard will hide let hideObserver = notificationCenter.addObserver( forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main ) { [weak self] notification in guard let self = self else { return } - // Extract data from notification synchronously on main queue - if let state = KeyboardState.from(notification: notification) { - self.updateState(state) + + let notificationData = NotificationData(from: notification) + + Task { @MainActor in + if let state = KeyboardState.from(notificationData: notificationData) { + self.updateState(state) + } } } From d3005a21059253d0278e4ce4c99cc1285cbcd16d Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 4 Feb 2026 08:48:59 +0200 Subject: [PATCH 35/51] fix: ThreadPostState --- .../Presentation/Comments/Thread/ThreadPostState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadPostState.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadPostState.swift index 671db6689..4e48288f5 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadPostState.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadPostState.swift @@ -7,7 +7,7 @@ import Foundation -public enum ThreadPostState { +public enum ThreadPostState: Sendable { case voted(id: String, voted: Bool, votesCount: Int) case flagged(id: String, flagged: Bool) case postAdded(id: String) From 787b2f073ad5405eda54ffa5fba5a9d9636d2387 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 4 Feb 2026 11:04:18 +0200 Subject: [PATCH 36/51] fix: fastlane xcode version increased --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index acb49e42f..8f5f2e7e0 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -15,7 +15,7 @@ update_fastlane before_all do xcodes( - version: '16.4', + version: '26.2', select_for_current_build_only: true, ) From 4bda6d9de7d239980ba6ba2711069f2bf667e359 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 5 Feb 2026 06:58:08 +0200 Subject: [PATCH 37/51] feat: fixes for core module --- Core/Core.xcodeproj/project.pbxproj | 16 ++++----- .../KeyboardAvoidingViewController.swift | 20 ++++++++--- .../State/Publishers+KeyboardState.swift | 34 ------------------ Core/Core/Configuration/Connectivity.swift | 14 +++++++- Core/Core/Network/DownloadManager.swift | 36 +++++++++++-------- Core/Core/Network/OfflineSyncManager.swift | 26 +++++++++----- Core/Core/View/Base/OfflineSnackBarView.swift | 9 ++--- .../Container/CourseContainerViewModel.swift | 19 ++++++---- 8 files changed, 88 insertions(+), 86 deletions(-) delete mode 100644 Core/Core/AvoidingHelpers/State/Publishers+KeyboardState.swift diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index eee669ae2..150c7377c 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -48,7 +48,6 @@ 027BD3AD2909475000392132 /* KeyboardScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3AA2909474F00392132 /* KeyboardScroller.swift */; }; 027BD3AE2909475000392132 /* KeyboardScrollerOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3AB2909474F00392132 /* KeyboardScrollerOptions.swift */; }; 027BD3AF2909475000392132 /* DismissKeyboardTapHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3AC2909475000392132 /* DismissKeyboardTapHandler.swift */; }; - 027BD3B32909475900392132 /* Publishers+KeyboardState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3B02909475800392132 /* Publishers+KeyboardState.swift */; }; 027BD3B42909475900392132 /* KeyboardState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3B12909475800392132 /* KeyboardState.swift */; }; 027BD3B52909475900392132 /* KeyboardStateObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3B22909475900392132 /* KeyboardStateObserver.swift */; }; 027BD3B82909476200392132 /* DismissKeyboardTapViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3B62909476200392132 /* DismissKeyboardTapViewModifier.swift */; }; @@ -250,7 +249,6 @@ 027BD3AA2909474F00392132 /* KeyboardScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardScroller.swift; sourceTree = ""; }; 027BD3AB2909474F00392132 /* KeyboardScrollerOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardScrollerOptions.swift; sourceTree = ""; }; 027BD3AC2909475000392132 /* DismissKeyboardTapHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissKeyboardTapHandler.swift; sourceTree = ""; }; - 027BD3B02909475800392132 /* Publishers+KeyboardState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Publishers+KeyboardState.swift"; sourceTree = ""; }; 027BD3B12909475800392132 /* KeyboardState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardState.swift; sourceTree = ""; }; 027BD3B22909475900392132 /* KeyboardStateObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardStateObserver.swift; sourceTree = ""; }; 027BD3B62909476200392132 /* DismissKeyboardTapViewModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissKeyboardTapViewModifier.swift; sourceTree = ""; }; @@ -478,7 +476,6 @@ children = ( 027BD3B12909475800392132 /* KeyboardState.swift */, 027BD3B22909475900392132 /* KeyboardStateObserver.swift */, - 027BD3B02909475800392132 /* Publishers+KeyboardState.swift */, ); path = State; sourceTree = ""; @@ -1064,7 +1061,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - CE953A3D2CD0DA940023D669 /* Generate Mockolo Mocks */ = { + 0770DE5A28D0B1E5006D8A5D /* SwiftGen */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -1074,16 +1071,16 @@ ); inputPaths = ( ); - name = "Generate Mockolo Mocks"; + name = SwiftGen; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which mockolo >/dev/null; then\n mockolo \\\n --sourcedirs \"${SRCROOT}/Core\" \\\n --destination \"${SRCROOT}/CoreTests/Generated/CoreMocks.generated.swift\" \\\n --mock-final \\\n --testable-imports \"Core\" \\\n --custom-imports \"Foundation\" \"SwiftUI\" \"Combine\"\nelse\n echo \"warning: mockolo not installed, download from https://github.com/uber/mockolo\"\nfi\n"; + shellScript = "if [[ -f \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" ]]; then\n \"${PODS_ROOT}/SwiftGen/bin/swiftgen\"\nelse\n echo \"warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; - 0770DE5A28D0B1E5006D8A5D /* SwiftGen */ = { + CE953A3D2CD0DA940023D669 /* Generate Mockolo Mocks */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -1093,14 +1090,14 @@ ); inputPaths = ( ); - name = SwiftGen; + name = "Generate Mockolo Mocks"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -f \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" ]]; then\n \"${PODS_ROOT}/SwiftGen/bin/swiftgen\"\nelse\n echo \"warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; + shellScript = "if which mockolo >/dev/null; then\n mockolo \\\n --sourcedirs \"${SRCROOT}/Core\" \\\n --destination \"${SRCROOT}/CoreTests/Generated/CoreMocks.generated.swift\" \\\n --mock-final \\\n --testable-imports \"Core\" \\\n --custom-imports \"Foundation\" \"SwiftUI\" \"Combine\"\nelse\n echo \"warning: mockolo not installed, download from https://github.com/uber/mockolo\"\nfi\n"; }; ED83AD5255805030E042D62A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -1191,7 +1188,6 @@ 06619EAF2B973B25001FAADE /* AccessibilityInjection.swift in Sources */, BAFB99822B0E2354007D09F9 /* FacebookConfig.swift in Sources */, 02935B732BCECAD000B22F66 /* Data_PrimaryEnrollment.swift in Sources */, - 027BD3B32909475900392132 /* Publishers+KeyboardState.swift in Sources */, 06DEA4A32BBD66A700110D20 /* BackNavigationButton.swift in Sources */, 0727877D28D25212002E9142 /* ProgressBar.swift in Sources */, CE09B2B62CE796AE0090DB53 /* InvalidCoreDataContextError.swift in Sources */, diff --git a/Core/Core/AvoidingHelpers/Avoider/KeyboardAvoidingViewController.swift b/Core/Core/AvoidingHelpers/Avoider/KeyboardAvoidingViewController.swift index 2ed85d65c..4081c6495 100644 --- a/Core/Core/AvoidingHelpers/Avoider/KeyboardAvoidingViewController.swift +++ b/Core/Core/AvoidingHelpers/Avoider/KeyboardAvoidingViewController.swift @@ -1,6 +1,5 @@ // -import Combine import SwiftUI import UIKit @@ -12,7 +11,7 @@ final class KeyboardAvoidingViewController: UIViewController { private var hostingController: UIHostingController private var bottomConstraint: NSLayoutConstraint? - private var keyboardStateCancellable: AnyCancellable? + @MainActor private var keyboardStateObserver: KeyboardStateObserver? private var keyboardState: KeyboardState = .default var bottomPadding: CGFloat = 0 { @@ -77,10 +76,21 @@ final class KeyboardAvoidingViewController: UIViewController { } private func subscribeToKeyboardPublisher() { - keyboardStateCancellable = Publishers.keyboardStatePublisher - .sink { [weak self] state in - self?.updateBottomConstraint(state: state) + Task { @MainActor [weak self] in + guard let self else { return } + let observer = KeyboardStateObserver() + self.keyboardStateObserver = observer + + withObservationTracking { + _ = observer.keyboardState + } onChange: { + Task { @MainActor [weak self] in + guard let self, let observer = self.keyboardStateObserver else { return } + self.updateBottomConstraint(state: observer.keyboardState) + self.subscribeToKeyboardPublisher() + } } + } } private func updateBottomConstraint(state: KeyboardState) { diff --git a/Core/Core/AvoidingHelpers/State/Publishers+KeyboardState.swift b/Core/Core/AvoidingHelpers/State/Publishers+KeyboardState.swift deleted file mode 100644 index e11689050..000000000 --- a/Core/Core/AvoidingHelpers/State/Publishers+KeyboardState.swift +++ /dev/null @@ -1,34 +0,0 @@ -// - -import Combine -import SwiftUI - -@MainActor -public extension Publishers { - static var keyboardStatePublisher: AnyPublisher { - let notificationCenter: NotificationCenter = .default - - let keyboardWillHide: NotificationCenter.Publisher = - notificationCenter.publisher(for: UIResponder.keyboardWillHideNotification) - - let keyboardWillChangeFrame: NotificationCenter.Publisher = - notificationCenter.publisher(for: UIResponder.keyboardWillChangeFrameNotification) - - let keyboardWillShow: NotificationCenter.Publisher = - notificationCenter.publisher(for: UIResponder.keyboardWillShowNotification) - - return Publishers.MergeMany( - keyboardWillHide, - keyboardWillChangeFrame, - keyboardWillShow - ) - .map { notification -> KeyboardState? in - KeyboardState.from(notification: notification) - } - .replaceNil(with: .default) - .removeDuplicates(by: { lhs, rhs -> Bool in - lhs.height == rhs.height - }) - .eraseToAnyPublisher() - } -} diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 0b5c57fdb..0f57ed721 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -20,8 +20,11 @@ public protocol ConnectivityProtocol: Sendable { var isInternetAvaliable: Bool { get } var isMobileData: Bool { get } var internetReachableSubject: CurrentValueSubject { get } + var internetState: InternetState? { get } } +@MainActor +@Observable public class Connectivity: ConnectivityProtocol { private let networkManager = NetworkReachabilityManager() @@ -32,12 +35,21 @@ public class Connectivity: ConnectivityProtocol { private var lastVerificationDate: TimeInterval? private var lastVerificationResult: Bool = true + // MARK: - Observable property (new way) + public private(set) var internetState: InternetState? { + didSet { + // Keep backward compatibility - update Combine subject + internetReachableSubject.send(internetState) + } + } + + // MARK: - Combine subject (for backward compatibility) public let internetReachableSubject = CurrentValueSubject(nil) private(set) var _isInternetAvailable: Bool = true { didSet { Task { @MainActor in - internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) + internetState = _isInternetAvailable ? .reachable : .notReachable } } } diff --git a/Core/Core/Network/DownloadManager.swift b/Core/Core/Network/DownloadManager.swift index e02ba47ab..219b5e4f0 100644 --- a/Core/Core/Network/DownloadManager.swift +++ b/Core/Core/Network/DownloadManager.swift @@ -247,21 +247,7 @@ public actor DownloadManager: DownloadManagerProtocol, @unchecked Sendable { } private func addObsevers() async { - await connectivity.internetReachableSubject - .sink {[weak self] state in - guard let self else { return } - Task { - switch state { - case .notReachable: - await self.waitingAll() - case .reachable: - try? await self.resumeDownloading() - case .none: - return - } - } - } - .store(in: &cancellables) + observeConnectivity() NotificationCenter.default.publisher(for: .tryDownloadAgain) .compactMap { $0.object as? [DownloadDataTask] } @@ -273,6 +259,26 @@ public actor DownloadManager: DownloadManagerProtocol, @unchecked Sendable { .store(in: &cancellables) } + nonisolated private func observeConnectivity() { + Task { @MainActor [connectivity] in + withObservationTracking { + _ = connectivity.internetState + } onChange: { + Task { [connectivity] in + switch await connectivity.internetState { + case .notReachable: + await self.waitingAll() + case .reachable: + try? await self.resumeDownloading() + case .none: + break + } + self.observeConnectivity() + } + } + } + } + private func tryDownloadAgain(downloads: [DownloadDataTask]) async { var tasksToInsert: [DownloadDataTask] = [] diff --git a/Core/Core/Network/OfflineSyncManager.swift b/Core/Core/Network/OfflineSyncManager.swift index bb4d995f5..1718cf68a 100644 --- a/Core/Core/Network/OfflineSyncManager.swift +++ b/Core/Core/Network/OfflineSyncManager.swift @@ -7,7 +7,6 @@ import Foundation @preconcurrency import WebKit -@preconcurrency import Combine import Swinject import OEXFoundation @@ -22,7 +21,6 @@ public class OfflineSyncManager: OfflineSyncManagerProtocol { let persistence: CorePersistenceProtocol let interactor: OfflineSyncInteractorProtocol let connectivity: ConnectivityProtocol - private var cancellables = Set() public init( persistence: CorePersistenceProtocol, @@ -33,16 +31,26 @@ public class OfflineSyncManager: OfflineSyncManagerProtocol { self.interactor = interactor self.connectivity = connectivity - self.connectivity.internetReachableSubject.sink(receiveValue: { state in - switch state { - case .reachable: - Task(priority: .low) { + observeConnectivity() + } + + private func observeConnectivity() { + withObservationTracking { + _ = connectivity.internetState + } onChange: { + Task { @MainActor [weak self] in + guard let self else { return } + + switch self.connectivity.internetState { + case .reachable: await self.syncOfflineProgress() + case .notReachable, nil: + break } - case .notReachable, nil: - break + + self.observeConnectivity() } - }).store(in: &cancellables) + } } public func handleMessage(message: WKScriptMessage, blockID: String) async { diff --git a/Core/Core/View/Base/OfflineSnackBarView.swift b/Core/Core/View/Base/OfflineSnackBarView.swift index ad6c70130..e6af859d8 100644 --- a/Core/Core/View/Base/OfflineSnackBarView.swift +++ b/Core/Core/View/Base/OfflineSnackBarView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import Combine import Theme public struct OfflineSnackBarView: View { @@ -62,18 +61,16 @@ public struct OfflineSnackBarView: View { .offset(y: dismiss ? 100 : 0) .opacity(dismiss ? 0 : 1) .transition(.move(edge: .bottom)) - .onReceive(connectivity.internetReachableSubject, perform: { state in + .onChange(of: connectivity.internetState) { _, state in switch state { case .notReachable: withAnimation { dismiss = false } - case .reachable: - break - case .none: + case .reachable, .none: break } - }) + } } } diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index c41bec882..adc06d558 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -1158,12 +1158,7 @@ extension CourseTab { } .store(in: &cancellables) - connectivity.internetReachableSubject - .sink { [weak self] _ in - guard let self else { return } - self.isInternetAvaliable = self.connectivity.isInternetAvaliable - } - .store(in: &cancellables) + observeConnectivity() NotificationCenter.default.addObserver( self, @@ -1188,6 +1183,18 @@ extension CourseTab { deinit { NotificationCenter.default.removeObserver(self) } + + private func observeConnectivity() { + withObservationTracking { + _ = connectivity.internetState + } onChange: { + Task { @MainActor [weak self] in + guard let self else { return } + self.isInternetAvaliable = self.connectivity.isInternetAvaliable + self.observeConnectivity() + } + } + } func handleVideoTap(video: CourseBlock, chapter: CourseChapter?) { // Find indices for navigation using full course structure From e1b75c017deb91180e267c3f61ed5e8447359f9d Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 5 Feb 2026 07:12:26 +0200 Subject: [PATCH 38/51] feat: fix for authorization --- .../Presentation/Registration/SignUpView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 4f9b3498b..801750243 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -18,9 +18,6 @@ public struct SignUpView: View { public init(viewModel: SignUpViewModel) { self.viewModel = viewModel - Task { - await viewModel.getRegistrationFields() - } } public var body: some View { @@ -196,6 +193,9 @@ public struct SignUpView: View { .ignoresSafeArea(.all, edges: .horizontal) .background(Theme.Colors.background.ignoresSafeArea(.all)) .navigationBarHidden(true) + .task { + await viewModel.getRegistrationFields() + } .onFirstAppear { viewModel.trackScreenEvent() } From 19c58ed917e5de28d46cd20f41b4f7f48cc10020 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 5 Feb 2026 08:16:43 +0200 Subject: [PATCH 39/51] feat: Course data race fix --- .../Container/CourseContainerViewModel.swift | 2 +- .../Container/CourseDownloadHelper.swift | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index adc06d558..b4f1d484f 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -149,7 +149,6 @@ extension CourseTab { selection: CourseTab = CourseTab.course, courseHelper: CourseDownloadHelperProtocol ) { - self.interactor = interactor self.authInteractor = authInteractor self.router = router @@ -258,6 +257,7 @@ extension CourseTab { self.courseStructure = courseStructure courseHelper.courseStructure = courseStructure + await courseHelper.refreshValue() update(from: courseHelper.value ?? .empty) diff --git a/Course/Course/Presentation/Container/CourseDownloadHelper.swift b/Course/Course/Presentation/Container/CourseDownloadHelper.swift index f844ca262..065bf7856 100644 --- a/Course/Course/Presentation/Container/CourseDownloadHelper.swift +++ b/Course/Course/Presentation/Container/CourseDownloadHelper.swift @@ -82,18 +82,21 @@ public final class CourseDownloadHelper: CourseDownloadHelperProtocol, @unchecke public init (courseStructure: CourseStructure?, manager: DownloadManagerProtocol) { self.manager = manager self.courseStructure = courseStructure + manager.eventPublisher() .sink { [weak self] state in guard let self else { return } self.queue.async {[weak self] in if case let .progress(currentTask) = state { - if let value = self?.value { - var newValue = value - newValue.setCurrentDownloadTask(task: currentTask) - self?.value = newValue + Task { @MainActor [weak self] in + if let value = self?.value { + var newValue = value + newValue.setCurrentDownloadTask(task: currentTask) + self?.value = newValue + } + self?.sourceProgressPublisher.send(currentTask) } - self?.sourceProgressPublisher.send(currentTask) return } @@ -114,8 +117,12 @@ public final class CourseDownloadHelper: CourseDownloadHelperProtocol, @unchecke } public func refreshValue() async { - guard let courseStructure else { return } + guard let courseStructure else { + return + } + let downloadTasks = await manager.getDownloadTasks() + await enumerate( tasks: downloadTasks, courseStructure: courseStructure, @@ -154,7 +161,10 @@ public final class CourseDownloadHelper: CourseDownloadHelperProtocol, @unchecke ) async { await withCheckedContinuation { continuation in queue.async {[weak self] in - guard let self else { return } + guard let self else { + continuation.resume() + return + } let notFinishedTasks: [DownloadDataTask] = tasks.filter { $0.state != .finished } .sorted(by: { $0.state.order < $1.state.order }) let courseDownloadTasks = tasks.filter { $0.courseId == courseStructure.id } @@ -253,11 +263,11 @@ public final class CourseDownloadHelper: CourseDownloadHelperProtocol, @unchecke state: downloadState ) - self.value = value - DispatchQueue.main.async { - self.sourcePublisher.send(value) + Task { @MainActor [weak self] in + self?.value = value + self?.sourcePublisher.send(value) } - + continuation.resume() } } From 3a1b45967a366f3eed2700b9f12943befc1c10fe Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 5 Feb 2026 09:27:06 +0200 Subject: [PATCH 40/51] fix: profile updates --- Core/CoreTests/Generated/CoreMocks.generated.swift | 3 +++ .../Presentation/DatesAndCalendar/CalendarManager.swift | 1 + .../Profile/UserProfile/UserProfileViewModel.swift | 1 + 3 files changed, 5 insertions(+) diff --git a/Core/CoreTests/Generated/CoreMocks.generated.swift b/Core/CoreTests/Generated/CoreMocks.generated.swift index 45a37fe58..c9298dca9 100644 --- a/Core/CoreTests/Generated/CoreMocks.generated.swift +++ b/Core/CoreTests/Generated/CoreMocks.generated.swift @@ -1411,6 +1411,9 @@ public final class CourseStructureManagerProtocolMock: CourseStructureManagerPro } public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Sendable { + + public var internetState: Core.InternetState? + public init() { } public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false) { self.isInternetAvaliable = isInternetAvaliable diff --git a/Profile/Profile/Presentation/DatesAndCalendar/CalendarManager.swift b/Profile/Profile/Presentation/DatesAndCalendar/CalendarManager.swift index fd29e33af..8c6fdeb93 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/CalendarManager.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/CalendarManager.swift @@ -15,6 +15,7 @@ import Core import OEXFoundation // MARK: - CalendarManager +@MainActor public final class CalendarManager: CalendarManagerProtocol { let eventStore = EKEventStore() diff --git a/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift index bad1ce1c2..61b08b56c 100644 --- a/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift +++ b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift @@ -9,6 +9,7 @@ import Core import SwiftUI @Observable +@MainActor public class UserProfileViewModel { public var userModel: UserProfile? From b941834970f20bc16a28a0e21bd775577494173b Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 5 Feb 2026 10:34:07 +0200 Subject: [PATCH 41/51] fix: discussions memory leaks fix --- .../Presentation/Comments/Thread/ThreadView.swift | 1 + .../Comments/Thread/ThreadViewModel.swift | 14 +++++++++++++- .../DiscussionSearchTopicsViewModel.swift | 6 ++++-- .../Discussion/Presentation/Posts/PostsView.swift | 3 +++ .../Presentation/Posts/PostsViewModel.swift | 14 +++++++++++++- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index c221e900f..26ed081d3 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -267,6 +267,7 @@ public struct ThreadView: View { .onDisappear { onBackTapped() viewModel.sendUpdateUnreadState() + viewModel.cleanup() } .edgesIgnoringSafeArea(.bottom) .background( diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index 646197f3e..35d9ca87f 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -16,6 +16,7 @@ public final class ThreadViewModel: BaseResponsesViewModel { @ObservationIgnored internal let threadStateSubject = CurrentValueSubject(nil) @ObservationIgnored private let postStateSubject: CurrentValueSubject + nonisolated(unsafe) private var observationTask: Task? public var isBlackedOut: Bool = false private let analytics: DiscussionAnalytics? @@ -32,8 +33,11 @@ public final class ThreadViewModel: BaseResponsesViewModel { super.init(interactor: interactor, router: router, config: config, storage: storage, analytics: analytics) - Task { + observationTask = Task { @MainActor in for await state in threadStateSubject.values { + if Task.isCancelled { + break + } guard let state = state else { continue } switch state { case let .voted(id, voted, votesCount): @@ -48,6 +52,14 @@ public final class ThreadViewModel: BaseResponsesViewModel { } } + deinit { + observationTask?.cancel() + } + + public func cleanup() { + observationTask?.cancel() + } + func generateComments(comments: [UserComment], thread: UserThread) -> Post { var result = Post( authorName: thread.author, diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift index 98efe4e43..868771868 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift @@ -31,7 +31,8 @@ public final class DiscussionSearchTopicsViewModel { private var nextPage = 1 private var totalPages = 1 - @ObservationIgnored private var searchTask: Task? + nonisolated(unsafe) private var searchTask: Task? + nonisolated(unsafe) private var observationTask: Task? // Keep CurrentValueSubject for now since it's passed to router @ObservationIgnored internal let postStateSubject = CurrentValueSubject(nil) @@ -62,7 +63,7 @@ public final class DiscussionSearchTopicsViewModel { self.debounceInterval = debounceInterval // Setup observer for postStateSubject - Task { + observationTask = Task { for await state in postStateSubject.values { guard let state = state else { continue } switch state { @@ -83,6 +84,7 @@ public final class DiscussionSearchTopicsViewModel { deinit { searchTask?.cancel() + observationTask?.cancel() } private func handleSearchTextChange(_ text: String) { diff --git a/Discussion/Discussion/Presentation/Posts/PostsView.swift b/Discussion/Discussion/Presentation/Posts/PostsView.swift index e512a3795..899356b6b 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsView.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsView.swift @@ -244,6 +244,9 @@ public struct PostsView: View { Theme.Colors.background .ignoresSafeArea() ) + .onDisappear { + viewModel.cleanup() + } } } diff --git a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift index 0e21c6a0d..491fa3e90 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift @@ -85,6 +85,7 @@ public final class PostsViewModel { private let config: ConfigProtocol private let storage: CoreStorage @ObservationIgnored internal let postStateSubject = CurrentValueSubject(nil) + nonisolated(unsafe) private var observationTask: Task? public init( interactor: DiscussionInteractorProtocol, @@ -97,8 +98,11 @@ public final class PostsViewModel { self.config = config self.storage = storage - Task { + observationTask = Task { @MainActor in for await state in postStateSubject.values { + if Task.isCancelled { + break + } guard let state = state else { continue } switch state { case let .followed(id, followed): @@ -116,11 +120,19 @@ public final class PostsViewModel { } } + deinit { + observationTask?.cancel() + } + public func resetPosts() { nextPage = 1 totalPages = 1 } + public func cleanup() { + observationTask?.cancel() + } + public func sort(by value: SortType) { self.sortTitle = value self.filteredPosts = self.discussionPosts From 0601c8d04a985fb91df30b81d9e1521b8168537b Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 5 Feb 2026 11:04:45 +0200 Subject: [PATCH 42/51] fix: Refactors DI screen assembly scope to avoid memory leaks --- .../ViewModifiers/KeyboardAvoidingModifier.swift | 7 ++++--- OpenEdX/DI/ScreenAssembly.swift | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Core/Core/AvoidingHelpers/ViewModifiers/KeyboardAvoidingModifier.swift b/Core/Core/AvoidingHelpers/ViewModifiers/KeyboardAvoidingModifier.swift index 85f0a04e8..eb56ce2cc 100644 --- a/Core/Core/AvoidingHelpers/ViewModifiers/KeyboardAvoidingModifier.swift +++ b/Core/Core/AvoidingHelpers/ViewModifiers/KeyboardAvoidingModifier.swift @@ -18,7 +18,7 @@ private struct KeyboardAvoidingModifier: ViewModifier { private let dismissKeyboardByTap: Bool private let onProvideScrollInvocator: ((KeyboardScrollInvocator) -> Void)? - private var keyboardObserver = KeyboardStateObserver() + @State private var keyboardObserver = KeyboardStateObserver() @State private var scrollInvocator = KeyboardScrollInvocator() init( @@ -62,8 +62,9 @@ private struct KeyboardAvoidingModifier: ViewModifier { } .onAppear { // Setup callback for manual scroll triggering - scrollInvocator.onTrigger = { [keyboardObserver, scrollerOptions, partialAvoidingPadding] in - guard !keyboardObserver.keyboardState.height.isZero, + scrollInvocator.onTrigger = { [weak keyboardObserver, scrollerOptions, partialAvoidingPadding] in + guard let keyboardObserver, + !keyboardObserver.keyboardState.height.isZero, let options = scrollerOptions else { return } diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index cc4a57875..81d0297be 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -165,6 +165,7 @@ class ScreenAssembly: Assembly { sourceScreen: sourceScreen ) } + .inObjectScope(.weak) container.register(ProgramWebviewViewModel.self) { @MainActor r in ProgramWebviewViewModel( @@ -176,6 +177,7 @@ class ScreenAssembly: Assembly { authInteractor: r.resolve(AuthInteractorProtocol.self)! ) } + .inObjectScope(.weak) container.register(SearchViewModel.self) { @MainActor r in SearchViewModel( @@ -214,6 +216,7 @@ class ScreenAssembly: Assembly { storage: r.resolve(CoreStorage.self)! ) } + .inObjectScope(.weak) container.register(PrimaryCourseDashboardViewModel.self) { @MainActor r in PrimaryCourseDashboardViewModel( @@ -266,6 +269,7 @@ class ScreenAssembly: Assembly { connectivity: r.resolve(ConnectivityProtocol.self)! ) } + .inObjectScope(.weak) container.register(EditProfileViewModel.self) { @MainActor r, userModel in EditProfileViewModel( userModel: userModel, @@ -355,6 +359,7 @@ class ScreenAssembly: Assembly { router: r.resolve(AppDatesRouter.self)! ) } + .inObjectScope(.weak) // MARK: Course container.register(CoursePersistenceProtocol.self) { r in @@ -753,6 +758,7 @@ class ScreenAssembly: Assembly { analytics: r.resolve(DownloadsAnalytics.self)! ) } + .inObjectScope(.weak) } } // swiftlint:enable function_body_length closure_parameter_position type_body_length From b840640f52e41a3442c821a36671f3ed42549aec Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 5 Feb 2026 11:54:29 +0200 Subject: [PATCH 43/51] fix: saves video progress before navigation, all courses memory leak --- Core/Core/Extensions/Notification.swift | 1 + Course/Course/Presentation/Unit/CourseUnitViewModel.swift | 3 +++ .../Course/Presentation/Video/VideoPlayerViewModel.swift | 7 +++++++ OpenEdX/DI/ScreenAssembly.swift | 1 + 4 files changed, 12 insertions(+) diff --git a/Core/Core/Extensions/Notification.swift b/Core/Core/Extensions/Notification.swift index 5e8dc6416..66bc6a8fb 100644 --- a/Core/Core/Extensions/Notification.swift +++ b/Core/Core/Extensions/Notification.swift @@ -25,4 +25,5 @@ public extension Notification.Name { static let refreshEnrollments = Notification.Name("refreshEnrollments") static let onVideoProgressUpdated = Notification.Name("onVideoProgressUpdated") static let onAssignmentProgressUpdated = Notification.Name("onAssignmentProgressUpdated") + static let saveVideoProgressBeforeNavigation = Notification.Name("saveVideoProgressBeforeNavigation") } diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index 0e618f6b3..c1ea02a38 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -499,6 +499,9 @@ public struct VerticalData: Equatable { let courseStructure = courseVideosStructure else { return } + + // Save current video progress before navigation + NotificationCenter.default.post(name: .saveVideoProgressBeforeNavigation, object: nil) // Track video click analytics analytics.courseVideoClicked( diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index b5ee6082c..e09d9d1a8 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -117,6 +117,13 @@ public class VideoPlayerViewModel { self?.trackVideoCompleted() } .store(in: &subscription) + + NotificationCenter.default.publisher(for: .saveVideoProgressBeforeNavigation) + .sink { [weak self] _ in + guard let self = self else { return } + self.saveCurrentProgress(duration: self.playerHolder.duration) + } + .store(in: &subscription) } diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 81d0297be..17e5744fe 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -238,6 +238,7 @@ class ScreenAssembly: Assembly { storage: r.resolve(CoreStorage.self)! ) } + .inObjectScope(.weak) // MARK: Profile From e389df14e1d8f407377257219cbfffa0ca3fa998 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 5 Feb 2026 11:58:39 +0200 Subject: [PATCH 44/51] fix: new test mocks --- .../Generated/AppDatesMocks.generated.swift | 6 +++++- .../Generated/AuthorizationMocks.generated.swift | 6 +++++- Core/CoreTests/Generated/CoreMocks.generated.swift | 9 +++++---- Course/CourseTests/Generated/CourseMocks.generated.swift | 6 +++++- .../Generated/DashboardMocks.generated.swift | 6 +++++- .../Generated/DiscoveryMocks.generated.swift | 6 +++++- .../Generated/DiscussionMocks.generated.swift | 6 +++++- .../Generated/DownloadsMocks.generated.swift | 6 +++++- .../ProfileTests/Generated/ProfileMocks.generated.swift | 6 +++++- 9 files changed, 45 insertions(+), 12 deletions(-) diff --git a/AppDates/AppDatesTests/Generated/AppDatesMocks.generated.swift b/AppDates/AppDatesTests/Generated/AppDatesMocks.generated.swift index 07e731e7a..1042e2911 100644 --- a/AppDates/AppDatesTests/Generated/AppDatesMocks.generated.swift +++ b/AppDates/AppDatesTests/Generated/AppDatesMocks.generated.swift @@ -2065,9 +2065,10 @@ public final class DatesRepositoryProtocolMock: DatesRepositoryProtocol, @unchec public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Sendable { public init() { } - public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false) { + public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false, internetState: InternetState? = nil) { self.isInternetAvaliable = isInternetAvaliable self.isMobileData = isMobileData + self.internetState = internetState } @@ -2083,6 +2084,9 @@ public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Se get { return _internetReachableSubject } set { _internetReachableSubject = newValue } } + + + public var internetState: InternetState? = nil } public final class DownloadManagerProtocolMock: DownloadManagerProtocol, @unchecked Sendable { diff --git a/Authorization/AuthorizationTests/Generated/AuthorizationMocks.generated.swift b/Authorization/AuthorizationTests/Generated/AuthorizationMocks.generated.swift index 9d91b858d..bf2c1a9ae 100644 --- a/Authorization/AuthorizationTests/Generated/AuthorizationMocks.generated.swift +++ b/Authorization/AuthorizationTests/Generated/AuthorizationMocks.generated.swift @@ -1760,9 +1760,10 @@ public final class AuthorizationRouterMock: AuthorizationRouter, @unchecked Send public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Sendable { public init() { } - public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false) { + public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false, internetState: InternetState? = nil) { self.isInternetAvaliable = isInternetAvaliable self.isMobileData = isMobileData + self.internetState = internetState } @@ -1778,6 +1779,9 @@ public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Se get { return _internetReachableSubject } set { _internetReachableSubject = newValue } } + + + public var internetState: InternetState? = nil } public final class AuthorizationAnalyticsMock: AuthorizationAnalytics { diff --git a/Core/CoreTests/Generated/CoreMocks.generated.swift b/Core/CoreTests/Generated/CoreMocks.generated.swift index c9298dca9..96d0533ea 100644 --- a/Core/CoreTests/Generated/CoreMocks.generated.swift +++ b/Core/CoreTests/Generated/CoreMocks.generated.swift @@ -1411,13 +1411,11 @@ public final class CourseStructureManagerProtocolMock: CourseStructureManagerPro } public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Sendable { - - public var internetState: Core.InternetState? - public init() { } - public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false) { + public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false, internetState: InternetState? = nil) { self.isInternetAvaliable = isInternetAvaliable self.isMobileData = isMobileData + self.internetState = internetState } @@ -1433,6 +1431,9 @@ public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Se get { return _internetReachableSubject } set { _internetReachableSubject = newValue } } + + + public var internetState: InternetState? = nil } public final class DownloadManagerProtocolMock: DownloadManagerProtocol, @unchecked Sendable { diff --git a/Course/CourseTests/Generated/CourseMocks.generated.swift b/Course/CourseTests/Generated/CourseMocks.generated.swift index 1fdb4fc3e..176f2245b 100644 --- a/Course/CourseTests/Generated/CourseMocks.generated.swift +++ b/Course/CourseTests/Generated/CourseMocks.generated.swift @@ -3303,9 +3303,10 @@ public final class PlayerViewControllerHolderProtocolMock: PlayerViewControllerH public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Sendable { public init() { } - public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false) { + public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false, internetState: InternetState? = nil) { self.isInternetAvaliable = isInternetAvaliable self.isMobileData = isMobileData + self.internetState = internetState } @@ -3321,6 +3322,9 @@ public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Se get { return _internetReachableSubject } set { _internetReachableSubject = newValue } } + + + public var internetState: InternetState? = nil } public final class CourseAnalyticsMock: CourseAnalytics { diff --git a/Dashboard/DashboardTests/Generated/DashboardMocks.generated.swift b/Dashboard/DashboardTests/Generated/DashboardMocks.generated.swift index c36c4bd2e..6ca250c64 100644 --- a/Dashboard/DashboardTests/Generated/DashboardMocks.generated.swift +++ b/Dashboard/DashboardTests/Generated/DashboardMocks.generated.swift @@ -1548,9 +1548,10 @@ public final class DashboardInteractorProtocolMock: DashboardInteractorProtocol, public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Sendable { public init() { } - public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false) { + public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false, internetState: InternetState? = nil) { self.isInternetAvaliable = isInternetAvaliable self.isMobileData = isMobileData + self.internetState = internetState } @@ -1566,6 +1567,9 @@ public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Se get { return _internetReachableSubject } set { _internetReachableSubject = newValue } } + + + public var internetState: InternetState? = nil } public final class DownloadManagerProtocolMock: DownloadManagerProtocol, @unchecked Sendable { diff --git a/Discovery/DiscoveryTests/Generated/DiscoveryMocks.generated.swift b/Discovery/DiscoveryTests/Generated/DiscoveryMocks.generated.swift index 058d227fc..ab3d0118a 100644 --- a/Discovery/DiscoveryTests/Generated/DiscoveryMocks.generated.swift +++ b/Discovery/DiscoveryTests/Generated/DiscoveryMocks.generated.swift @@ -2207,9 +2207,10 @@ public final class DiscoveryRepositoryProtocolMock: DiscoveryRepositoryProtocol, public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Sendable { public init() { } - public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false) { + public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false, internetState: InternetState? = nil) { self.isInternetAvaliable = isInternetAvaliable self.isMobileData = isMobileData + self.internetState = internetState } @@ -2225,6 +2226,9 @@ public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Se get { return _internetReachableSubject } set { _internetReachableSubject = newValue } } + + + public var internetState: InternetState? = nil } public final class DownloadManagerProtocolMock: DownloadManagerProtocol, @unchecked Sendable { diff --git a/Discussion/DiscussionTests/Generated/DiscussionMocks.generated.swift b/Discussion/DiscussionTests/Generated/DiscussionMocks.generated.swift index 6c5a824f4..799542908 100644 --- a/Discussion/DiscussionTests/Generated/DiscussionMocks.generated.swift +++ b/Discussion/DiscussionTests/Generated/DiscussionMocks.generated.swift @@ -2297,9 +2297,10 @@ public final class DiscussionRouterMock: DiscussionRouter, @unchecked Sendable { public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Sendable { public init() { } - public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false) { + public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false, internetState: InternetState? = nil) { self.isInternetAvaliable = isInternetAvaliable self.isMobileData = isMobileData + self.internetState = internetState } @@ -2315,6 +2316,9 @@ public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Se get { return _internetReachableSubject } set { _internetReachableSubject = newValue } } + + + public var internetState: InternetState? = nil } public final class DownloadManagerProtocolMock: DownloadManagerProtocol, @unchecked Sendable { diff --git a/Downloads/DownloadsTests/Generated/DownloadsMocks.generated.swift b/Downloads/DownloadsTests/Generated/DownloadsMocks.generated.swift index 2d10940fd..69f85c6fa 100644 --- a/Downloads/DownloadsTests/Generated/DownloadsMocks.generated.swift +++ b/Downloads/DownloadsTests/Generated/DownloadsMocks.generated.swift @@ -1642,9 +1642,10 @@ public final class DownloadsHelperProtocolMock: DownloadsHelperProtocol, @unchec public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Sendable { public init() { } - public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false) { + public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false, internetState: InternetState? = nil) { self.isInternetAvaliable = isInternetAvaliable self.isMobileData = isMobileData + self.internetState = internetState } @@ -1660,6 +1661,9 @@ public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Se get { return _internetReachableSubject } set { _internetReachableSubject = newValue } } + + + public var internetState: InternetState? = nil } public final class DownloadManagerProtocolMock: DownloadManagerProtocol, @unchecked Sendable { diff --git a/Profile/ProfileTests/Generated/ProfileMocks.generated.swift b/Profile/ProfileTests/Generated/ProfileMocks.generated.swift index 52fa772a7..3a6a058c9 100644 --- a/Profile/ProfileTests/Generated/ProfileMocks.generated.swift +++ b/Profile/ProfileTests/Generated/ProfileMocks.generated.swift @@ -2593,9 +2593,10 @@ public final class ProfileAnalyticsMock: ProfileAnalytics { public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Sendable { public init() { } - public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false) { + public init(isInternetAvaliable: Bool = false, isMobileData: Bool = false, internetState: InternetState? = nil) { self.isInternetAvaliable = isInternetAvaliable self.isMobileData = isMobileData + self.internetState = internetState } @@ -2611,6 +2612,9 @@ public final class ConnectivityProtocolMock: ConnectivityProtocol, @unchecked Se get { return _internetReachableSubject } set { _internetReachableSubject = newValue } } + + + public var internetState: InternetState? = nil } public final class DownloadManagerProtocolMock: DownloadManagerProtocol, @unchecked Sendable { From d94d6dba97fea7d8507f668661cf9958dfb3edf2 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Mon, 9 Feb 2026 16:00:05 +0200 Subject: [PATCH 45/51] fix: keyboard bug --- .gitignore | 3 +++ .../Avoider/KeyboardAvoidingViewController.swift | 12 ++++++++---- OpenEdX.xcodeproj/project.pbxproj | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 75a5357e4..1bf01033f 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,6 @@ I18N/ *.lproj/ !en.lproj/ /config_script/__pycache__ + +# AI Assistant Configuration +CLAUDE.md diff --git a/Core/Core/AvoidingHelpers/Avoider/KeyboardAvoidingViewController.swift b/Core/Core/AvoidingHelpers/Avoider/KeyboardAvoidingViewController.swift index 4081c6495..3b4170549 100644 --- a/Core/Core/AvoidingHelpers/Avoider/KeyboardAvoidingViewController.swift +++ b/Core/Core/AvoidingHelpers/Avoider/KeyboardAvoidingViewController.swift @@ -78,14 +78,18 @@ final class KeyboardAvoidingViewController: UIViewController { private func subscribeToKeyboardPublisher() { Task { @MainActor [weak self] in guard let self else { return } - let observer = KeyboardStateObserver() - self.keyboardStateObserver = observer + + if self.keyboardStateObserver == nil { + self.keyboardStateObserver = KeyboardStateObserver() + } + + guard let observer = self.keyboardStateObserver else { return } withObservationTracking { _ = observer.keyboardState } onChange: { - Task { @MainActor [weak self] in - guard let self, let observer = self.keyboardStateObserver else { return } + Task { @MainActor in + guard let observer = self.keyboardStateObserver else { return } self.updateBottomConstraint(state: observer.keyboardState) self.subscribeToKeyboardPublisher() } diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 9f1049c4b..51bfc2b81 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -901,7 +901,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -993,7 +993,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; From 6cf8ccaf62edb100395455dbd3199d544b63a492 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 10 Feb 2026 14:51:42 +0200 Subject: [PATCH 46/51] fix: full profile edit logic --- .../EditProfile/EditProfileViewModel.swift | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift index 04f178c79..b2e5acaef 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift @@ -173,7 +173,14 @@ public class EditProfileViewModel { } else { yearOfBirth = userModel.yearOfBirth } - if yearOfBirth == 0 || currentYear - yearOfBirth < minimumFullAccountAge { + + if yearOfBirth == 0 { + if profileChanges.profileType == .limited { + alertMessage = ProfileLocalization.Edit.tooYongUser + } else { + profileChanges.profileType.toggle() + } + } else if currentYear - yearOfBirth < minimumFullAccountAge { alertMessage = ProfileLocalization.Edit.tooYongUser } else { profileChanges.profileType.toggle() @@ -183,30 +190,27 @@ public class EditProfileViewModel { } func checkProfileType() { + let yearOfBirth: Int if yearsConfiguration.text != "" { - let yearOfBirth = yearsConfiguration.text - if currentYear - (Int(yearOfBirth) ?? 0) < minimumFullAccountAge { - profileChanges.profileType = .limited - isYongUser = true - } else { - withAnimation { - isYongUser = false - } - } + yearOfBirth = Int(yearsConfiguration.text) ?? 0 } else { - if (currentYear - userModel.yearOfBirth) < minimumFullAccountAge { - profileChanges.profileType = .limited - isYongUser = true - } else { - withAnimation { - isYongUser = false - } - } + yearOfBirth = userModel.yearOfBirth } - if profileChanges.profileType == .full { - isEditable = true - } else { + + if yearOfBirth == 0 { + withAnimation { + isYongUser = false + } isEditable = false + } else if currentYear - yearOfBirth < minimumFullAccountAge { + profileChanges.profileType = .limited + isYongUser = true + isEditable = false + } else { + withAnimation { + isYongUser = false + } + isEditable = profileChanges.profileType == .full } } @@ -325,6 +329,7 @@ public class EditProfileViewModel { } generateFieldConfigurations() + checkProfileType() } private func generateYears() { From 17a6e933b4407313120e0c27caef7cb3ffb14982 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 10 Feb 2026 15:49:10 +0200 Subject: [PATCH 47/51] fix: scroll optimization --- .../Subviews/CustomDisclosureGroup.swift | 464 ++++++++++-------- 1 file changed, 246 insertions(+), 218 deletions(-) diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index 40c835205..13ab0ecc2 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -10,223 +10,206 @@ import Core import Theme struct CustomDisclosureGroup: View { - private let proxy: GeometryProxy + private let proxyWidth: CGFloat private let course: CourseStructure private let viewModel: CourseContainerViewModel - private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + private let idiom: UIUserInterfaceIdiom init(course: CourseStructure, proxy: GeometryProxy, viewModel: CourseContainerViewModel) { self.course = course - self.proxy = proxy + self.proxyWidth = proxy.size.width self.viewModel = viewModel + self.idiom = UIDevice.current.userInterfaceIdiom } var body: some View { VStack(alignment: .leading, spacing: 8) { - ForEach(course.childs) { chapter in - let chapterIndex = course.childs.firstIndex(where: { $0.id == chapter.id }) - VStack(alignment: .leading, spacing: 0) { - // MARK: - Progress Bar - SectionProgressView(progress: chapterProgress(for: chapter)) - .padding(.horizontal, -16) - .padding(.top, -12) - - Button( - action: { - withAnimation(.linear(duration: course.childs.count > 1 ? 0.2 : 0.05)) { - viewModel.expandedSections[chapter.id, default: false].toggle() - } - viewModel.trackSectionClicked(chapter) - }, label: { - HStack { - CoreAssets.chevronRight.swiftUIImage - .rotationEffect( - .degrees(viewModel.expandedSections[chapter.id] ?? false ? -90 : 90) - ) - .foregroundColor(Theme.Colors.textPrimary) - if chapter.childs.allSatisfy({ $0.completion == 1 }) { - CoreAssets.finishedSequence.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.success) - } - Text(chapter.displayName) - .font(Theme.Fonts.titleMedium) - .foregroundColor(Theme.Colors.textPrimary) - .lineLimit(1) - Spacer() - if canDownloadAllSections(in: chapter), - let state = downloadAllButtonState(for: chapter) { - Button( - action: { - downloadAllSubsections(in: chapter, state: state) - }, label: { - switch state { - case .available: - DownloadAvailableView() - case .downloading: - DownloadProgressView() - case .finished: - DownloadFinishedView() - } - - } - ) - } - } - } - ) - .padding(.top, 8) - if viewModel.expandedSections[chapter.id] ?? false { - VStack(alignment: .leading) { - ForEach(chapter.childs) { sequential in - let sequentialIndex = chapter.childs.firstIndex(where: { $0.id == sequential.id }) - VStack(alignment: .leading) { - HStack { - Button( - action: { - guard let chapterIndex = chapterIndex else { return } - guard let sequentialIndex else { return } - guard let courseVertical = sequential.childs.first else { return } - guard let block = courseVertical.childs.first else { - viewModel.router.showGatedContentError(url: courseVertical.webUrl) - return - } - - viewModel.trackSequentialClicked(sequential) - if viewModel.config.uiComponents.courseDropDownNavigationEnabled { - viewModel.router.showCourseUnit( - courseName: viewModel.courseStructure?.displayName ?? "", - blockId: block.id, - courseID: viewModel.courseStructure?.id ?? "", - verticalIndex: 0, - chapters: course.childs, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex, - showVideoNavigation: false, - courseVideoStructure: nil - ) - } else { - viewModel.router.showCourseVerticalView( - courseID: viewModel.courseStructure?.id ?? "", - courseName: viewModel.courseStructure?.displayName ?? "", - title: sequential.displayName, - chapters: course.childs, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) - } - }, - label: { - VStack(alignment: .leading) { - HStack { - if sequential.completion == 1 { - CoreAssets.finishedSequence.swiftUIImage - .renderingMode(.template) - .resizable() - .foregroundColor(Theme.Colors.success) - .frame(width: 20, height: 20) - } else { - sequential.type.image - } - Text(sequential.displayName) - .font(Theme.Fonts.titleSmall) - .multilineTextAlignment(.leading) - .lineLimit(1) - .frame( - maxWidth: idiom == .pad - ? proxy.size.width * 0.5 - : proxy.size.width * 0.6, - alignment: .leading - ) - } - if let assignmentStatusText = assignmentStatusText( - sequential: sequential - ) { - Text(assignmentStatusText) - .font(Theme.Fonts.bodySmall) - .multilineTextAlignment(.leading) - .lineLimit(2) - } - } - .foregroundColor(Theme.Colors.textPrimary) - .accessibilityElement(children: .ignore) - .accessibilityLabel(sequential.displayName) - } - ) - Spacer() - if sequential.due != nil { - CoreAssets.chevronRight.swiftUIImage - .foregroundColor(Theme.Colors.textPrimary) - } - } - .padding(.vertical, 4) - } - } - } - .padding(.top, 8) - } - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background( - RoundedRectangle(cornerRadius: 6) - .fill(Theme.Colors.datesSectionBackground) - ) - .overlay( - RoundedRectangle(cornerRadius: 6) - .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) - .foregroundColor(Theme.Colors.cardViewStroke) + ForEach(Array(course.childs.enumerated()), id: \.element.id) { chapterIndex, chapter in + ChapterRowView( + chapter: chapter, + chapterIndex: chapterIndex, + course: course, + proxyWidth: proxyWidth, + idiom: idiom, + viewModel: viewModel ) } } .padding(.horizontal, 24) .padding(.vertical, 8) + } +} +// MARK: - Chapter Row +private struct ChapterRowView: View { + let chapter: CourseChapter + let chapterIndex: Int + let course: CourseStructure + let proxyWidth: CGFloat + let idiom: UIUserInterfaceIdiom + let viewModel: CourseContainerViewModel + + private var isExpanded: Bool { + viewModel.expandedSections[chapter.id] ?? false } - private func deleteMessage(for chapter: CourseChapter) -> String { - "\(CourseLocalization.Alert.deleteVideos) \"\(chapter.displayName)\"?" + private var isChapterCompleted: Bool { + chapter.childs.allSatisfy { $0.completion == 1 } } - func getAssignmentStatus(for date: Date) -> String { - let calendar = Calendar.current - let today = Date() + var body: some View { + VStack(alignment: .leading, spacing: 0) { + SectionProgressView(progress: viewModel.chapterProgress(for: chapter)) + .padding(.horizontal, -16) + .padding(.top, -12) + + Button( + action: { + withAnimation(.linear(duration: course.childs.count > 1 ? 0.2 : 0.05)) { + viewModel.expandedSections[chapter.id, default: false].toggle() + } + viewModel.trackSectionClicked(chapter) + }, label: { + HStack { + CoreAssets.chevronRight.swiftUIImage + .rotationEffect( + .degrees(isExpanded ? -90 : 90) + ) + .foregroundColor(Theme.Colors.textPrimary) + if isChapterCompleted { + CoreAssets.finishedSequence.swiftUIImage.renderingMode(.template) + .foregroundColor(Theme.Colors.success) + } + Text(chapter.displayName) + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textPrimary) + .lineLimit(1) + Spacer() + if let state = downloadAllButtonState { + Button( + action: { + downloadAllSubsections(state: state) + }, label: { + switch state { + case .available: + DownloadAvailableView() + case .downloading: + DownloadProgressView() + case .finished: + DownloadFinishedView() + } + + } + ) + } + } + } + ) + .padding(.top, 8) + if isExpanded { + VStack(alignment: .leading) { + ForEach(Array(chapter.childs.enumerated()), id: \.element.id) { sequentialIndex, sequential in + SequentialRowView( + sequential: sequential, + sequentialIndex: sequentialIndex, + chapterIndex: chapterIndex, + course: course, + proxyWidth: proxyWidth, + idiom: idiom, + viewModel: viewModel + ) + } + } + .padding(.top, 8) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(Theme.Colors.datesSectionBackground) + ) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) + .foregroundColor(Theme.Colors.cardViewStroke) + ) + } + + private var downloadAllButtonState: DownloadViewState? { + guard canDownloadAllSections else { return nil } - if calendar.isDateInToday(date) { - return CourseLocalization.Course.dueToday - } else if calendar.isDateInTomorrow(date) { - return CourseLocalization.Course.dueTomorrow - } else if let daysUntil = calendar.dateComponents([.day], from: today, to: date).day, daysUntil > 0 { - return CourseLocalization.dueIn(daysUntil) - } else if let daysAgo = calendar.dateComponents([.day], from: date, to: today).day, daysAgo > 0 { - return CourseLocalization.pastDue(daysAgo) + var downloads: [DownloadViewState] = [] + for sequential in chapter.childs { + if let state = viewModel.sequentialsDownloadState[sequential.id] { + downloads.append(state) + } + } + if downloads.contains(.downloading) { + return .downloading + } else if downloads.allSatisfy({ $0 == .finished }) { + return .finished } else { - return "" + return .available } } - private func canDownloadAllSections(in chapter: CourseChapter) -> Bool { + private var canDownloadAllSections: Bool { chapter.childs.contains { sequential in - sequentialDownloadState(sequential) != nil + viewModel.sequentialsDownloadState[sequential.id] != nil + } + } + + private func downloadAllSubsections(state: DownloadViewState) { + Task { + var allBlocks: [CourseBlock] = [] + var sequentialsToDownload: [CourseSequential] = [] + for sequential in chapter.childs { + let blocks = await viewModel.collectBlocks( + chapter: chapter, + blockId: sequential.id, + state: state + ) + if !blocks.isEmpty { + allBlocks.append(contentsOf: blocks) + sequentialsToDownload.append(sequential) + } + } + await viewModel.download( + state: state, + blocks: allBlocks, + sequentials: sequentialsToDownload + ) } } +} - private func assignmentStatusText( - sequential: CourseSequential - ) -> String? { +// MARK: - Sequential Row +private struct SequentialRowView: View { + let sequential: CourseSequential + let sequentialIndex: Int + let chapterIndex: Int + let course: CourseStructure + let proxyWidth: CGFloat + let idiom: UIUserInterfaceIdiom + let viewModel: CourseContainerViewModel + + private var maxWidth: CGFloat { + idiom == .pad ? proxyWidth * 0.5 : proxyWidth * 0.6 + } + + private var assignmentText: String? { var parts: [String] = [] - // Name if let name = sequential.sequentialProgress?.assignmentType, !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { parts.append(name) } - // Deadline if let due = sequential.due { parts.append(getAssignmentStatus(for: due)) } - // Progress if let sp = sequential.sequentialProgress, let earned = sp.numPointsEarned, let possible = sp.numPointsPossible, @@ -237,54 +220,99 @@ struct CustomDisclosureGroup: View { return parts.isEmpty ? nil : parts.joined(separator: " - ") } - private func downloadAllSubsections(in chapter: CourseChapter, state: DownloadViewState) { - Task { - var allBlocks: [CourseBlock] = [] - var sequentialsToDownload: [CourseSequential] = [] - for sequential in chapter.childs { - let blocks = await viewModel.collectBlocks( - chapter: chapter, - blockId: sequential.id, - state: state + var body: some View { + VStack(alignment: .leading) { + HStack { + Button( + action: { + guard let courseVertical = sequential.childs.first else { return } + guard let block = courseVertical.childs.first else { + viewModel.router.showGatedContentError(url: courseVertical.webUrl) + return + } + + viewModel.trackSequentialClicked(sequential) + if viewModel.config.uiComponents.courseDropDownNavigationEnabled { + viewModel.router.showCourseUnit( + courseName: viewModel.courseStructure?.displayName ?? "", + blockId: block.id, + courseID: viewModel.courseStructure?.id ?? "", + verticalIndex: 0, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex, + showVideoNavigation: false, + courseVideoStructure: nil + ) + } else { + viewModel.router.showCourseVerticalView( + courseID: viewModel.courseStructure?.id ?? "", + courseName: viewModel.courseStructure?.displayName ?? "", + title: sequential.displayName, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + } + }, + label: { + VStack(alignment: .leading) { + HStack { + if sequential.completion == 1 { + CoreAssets.finishedSequence.swiftUIImage + .renderingMode(.template) + .resizable() + .foregroundColor(Theme.Colors.success) + .frame(width: 20, height: 20) + } else { + sequential.type.image + } + Text(sequential.displayName) + .font(Theme.Fonts.titleSmall) + .multilineTextAlignment(.leading) + .lineLimit(1) + .frame( + maxWidth: maxWidth, + alignment: .leading + ) + } + if let assignmentText { + Text(assignmentText) + .font(Theme.Fonts.bodySmall) + .multilineTextAlignment(.leading) + .lineLimit(2) + } + } + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(sequential.displayName) + } ) - if !blocks.isEmpty { - allBlocks.append(contentsOf: blocks) - sequentialsToDownload.append(sequential) + Spacer() + if sequential.due != nil { + CoreAssets.chevronRight.swiftUIImage + .foregroundColor(Theme.Colors.textPrimary) } } - await viewModel.download( - state: state, - blocks: allBlocks, - sequentials: sequentialsToDownload - ) + .padding(.vertical, 4) } } - private func downloadAllButtonState(for chapter: CourseChapter) -> DownloadViewState? { - if canDownloadAllSections(in: chapter) { - var downloads: [DownloadViewState] = [] - for sequential in chapter.childs { - if let state = sequentialDownloadState(sequential) { - downloads.append(state) - } - } - if downloads.contains(.downloading) { - return .downloading - } else if downloads.allSatisfy({ $0 == .finished }) { - return .finished - } else { - return .available - } + private func getAssignmentStatus(for date: Date) -> String { + let calendar = Calendar.current + let today = Date() + + if calendar.isDateInToday(date) { + return CourseLocalization.Course.dueToday + } else if calendar.isDateInTomorrow(date) { + return CourseLocalization.Course.dueTomorrow + } else if let daysUntil = calendar.dateComponents([.day], from: today, to: date).day, daysUntil > 0 { + return CourseLocalization.dueIn(daysUntil) + } else if let daysAgo = calendar.dateComponents([.day], from: date, to: today).day, daysAgo > 0 { + return CourseLocalization.pastDue(daysAgo) + } else { + return "" } - return nil - } - - private func sequentialDownloadState(_ sequential: CourseSequential) -> DownloadViewState? { - return viewModel.sequentialsDownloadState[sequential.id] - } - - private func chapterProgress(for chapter: CourseChapter) -> Double { - return viewModel.chapterProgress(for: chapter) } } From f26f372963a308b74f782392af369536d5d392b9 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 10 Feb 2026 16:15:00 +0200 Subject: [PATCH 48/51] fix: view model deinit bug --- .../Container/CourseContainerViewModel.swift | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index b4f1d484f..011a7f5ff 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -1179,21 +1179,15 @@ extension CourseTab { } .store(in: &cancellables) } - - deinit { - NotificationCenter.default.removeObserver(self) - } private func observeConnectivity() { - withObservationTracking { - _ = connectivity.internetState - } onChange: { - Task { @MainActor [weak self] in + connectivity.internetReachableSubject + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in guard let self else { return } self.isInternetAvaliable = self.connectivity.isInternetAvaliable - self.observeConnectivity() } - } + .store(in: &cancellables) } func handleVideoTap(video: CourseBlock, chapter: CourseChapter?) { From cf076cfe6a8711bf97758b0917f72305b17554b3 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 10 Feb 2026 17:11:40 +0200 Subject: [PATCH 49/51] fix: video navigation optimization --- .../Presentation/Unit/CourseUnitView.swift | 6 - .../Unit/CourseUnitViewModel.swift | 105 ++++++++++-------- .../Unit/Subviews/VideoNavigationView.swift | 4 +- .../Video/VideoPlayerViewModel.swift | 2 - OpenEdX/Router.swift | 4 +- 5 files changed, 64 insertions(+), 57 deletions(-) diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 11b3d08c9..2e1476316 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -190,12 +190,6 @@ public struct CourseUnitView: View { currentBlock: $currentBlock, block: block ) - .onReceive(NotificationCenter.default.publisher(for: - .onVideoProgressUpdated)) { _ in - Task { - await viewModel.getCourseVideoBlocks() - } - } } } diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index c1ea02a38..3a79df04d 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -146,6 +146,7 @@ public struct VerticalData: Equatable { public var allVideosFetched = false public var isVideosForNavigationLoading: Bool = false var currentVideoIndex: Int? + private var videoBlocksTask: Task? var lessonID: String var courseID: String @@ -419,54 +420,65 @@ public struct VerticalData: Equatable { @MainActor func getCourseVideoBlocks() async { + videoBlocksTask?.cancel() + + if isVideosForNavigationLoading { + return + } + + if !allVideosForNavigation.isEmpty && courseVideosStructure != nil { + return + } isVideosForNavigationLoading = true - defer { - Task { @MainActor in - try? await Task.sleep(for: .seconds(0.2)) - self.isVideosForNavigationLoading = false + videoBlocksTask = Task { @MainActor in + defer { + Task { @MainActor in + try? await Task.sleep(for: .seconds(0.2)) + self.isVideosForNavigationLoading = false + } } - } - if let courseVideosStructure { - do { - let videoFromCourse = await interactor.getCourseVideoBlocks(fullStructure: courseVideosStructure) + if let courseVideosStructure { + do { + let videoFromCourse = await interactor.getCourseVideoBlocks(fullStructure: courseVideosStructure) - allVideosForNavigation = try await interactor.getAllVideosForNavigation( - structure: videoFromCourse - ) + allVideosForNavigation = try await interactor.getAllVideosForNavigation( + structure: videoFromCourse + ) - return + return - } catch { - print("Failed to get all videos for course: \(error.localizedDescription)") + } catch { + } } - } - async let structureTask = getCourseStructure(courseID: courseID) + async let structureTask = getCourseStructure(courseID: courseID) - do { - guard let courseStructure = try await structureTask else { - throw NSError( - domain: "GetCourseBlocks", - code: 0, - userInfo: [NSLocalizedDescriptionKey: "Course structure is nil"] - ) - } + do { + guard let courseStructure = try await structureTask else { + throw NSError( + domain: "GetCourseBlocks", + code: 0, + userInfo: [NSLocalizedDescriptionKey: "Course structure is nil"] + ) + } - async let videosTask = interactor.getCourseVideoBlocks(fullStructure: courseStructure) - courseVideosStructure = await videosTask + async let videosTask = interactor.getCourseVideoBlocks(fullStructure: courseStructure) + courseVideosStructure = await videosTask - if let courseVideosStructure { - allVideosForNavigation = try await interactor.getAllVideosForNavigation( - structure: courseVideosStructure - ) - } + if let courseVideosStructure { + allVideosForNavigation = try await interactor.getAllVideosForNavigation( + structure: courseVideosStructure + ) + } - } catch { - print("Failed to load course blocks: \(error.localizedDescription)") - courseVideosStructure = nil + } catch { + courseVideosStructure = nil + } } + + await videoBlocksTask?.value } func createBreadCrumpsForVideoNavigation(video: CourseBlock) -> String { @@ -486,13 +498,12 @@ public struct VerticalData: Equatable { } } } - .first + .first ?? "" - return breadcrumb ?? "" + return breadcrumb } func handleVideoTap(video: CourseBlock) { - // Find indices for navigation using full course structure guard let chapterIndex = findChapterIndexInFullStructure(video: video), let sequentialIndex = findSequentialIndexInFullStructure(video: video), let verticalIndex = findVerticalIndexInFullStructure(video: video), @@ -500,10 +511,8 @@ public struct VerticalData: Equatable { return } - // Save current video progress before navigation NotificationCenter.default.post(name: .saveVideoProgressBeforeNavigation, object: nil) - // Track video click analytics analytics.courseVideoClicked( courseId: courseStructure.id, courseName: courseStructure.displayName, @@ -526,22 +535,25 @@ public struct VerticalData: Equatable { } private func findChapterIndexInFullStructure(video: CourseBlock) -> Int? { - guard let courseStructure = courseVideosStructure else { return nil } + guard let courseStructure = courseVideosStructure else { + return nil + } - // Find the chapter that contains this video in the full structure - return courseStructure.childs.firstIndex { fullChapter in + let index = courseStructure.childs.firstIndex { fullChapter in fullChapter.childs.contains { sequential in sequential.childs.contains { vertical in vertical.childs.contains { $0.id == video.id } } } } + return index } private func findSequentialIndexInFullStructure(video: CourseBlock) -> Int? { - guard let courseStructure = courseVideosStructure else { return nil } + guard let courseStructure = courseVideosStructure else { + return nil + } - // Find the chapter and sequential that contains this video in the full structure for fullChapter in courseStructure.childs { if let sequentialIndex = fullChapter.childs.firstIndex(where: { sequential in sequential.childs.contains { vertical in @@ -555,9 +567,10 @@ public struct VerticalData: Equatable { } private func findVerticalIndexInFullStructure(video: CourseBlock) -> Int? { - guard let courseStructure = courseVideosStructure else { return nil } + guard let courseStructure = courseVideosStructure else { + return nil + } - // Find the vertical that contains this video in the full structure for fullChapter in courseStructure.childs { for sequential in fullChapter.childs { if let verticalIndex = sequential.childs.firstIndex(where: { vertical in diff --git a/Course/Course/Presentation/Unit/Subviews/VideoNavigationView.swift b/Course/Course/Presentation/Unit/Subviews/VideoNavigationView.swift index 7b8c04473..0760c0a36 100644 --- a/Course/Course/Presentation/Unit/Subviews/VideoNavigationView.swift +++ b/Course/Course/Presentation/Unit/Subviews/VideoNavigationView.swift @@ -132,7 +132,9 @@ struct VideoNavigationView: View { scrollTo(currentVideo.id) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - guard let scroll = uiScrollView else { return } + guard let scroll = uiScrollView else { + return + } let newX = max(scroll.contentOffset.x - 20, 0) scroll.setContentOffset(CGPoint(x: newX, y: 0), animated: true) } diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index e09d9d1a8..a235f9078 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -272,10 +272,8 @@ public class VideoPlayerViewModel { } public func saveCurrentProgress(duration: TimeInterval) { - Task { let time = currentTime -// let duration = playerHolder.duration if duration > 0 && time > 0 { let progress = min(time / duration, 1.0) diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 1c5af4b0b..2da89bd63 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -531,7 +531,8 @@ public class Router: AuthorizationRouter, let isDropdownActive = config?.uiComponents.courseDropDownNavigationEnabled ?? false let view = CourseUnitView(viewModel: viewModel, isDropdownActive: isDropdownActive) - return UIHostingController(rootView: view) + let controller = UIHostingController(rootView: view) + return controller } public func showCourseComponent( @@ -617,7 +618,6 @@ public class Router: AuthorizationRouter, showVideoNavigation: Bool, courseVideoStructure: CourseStructure? ) { - let controllerUnit = getUnitController( courseName: courseName, blockId: blockId, From 0a6d5d5aab737f9005c6a9ed088abbd42d72e687 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 10 Feb 2026 17:18:48 +0200 Subject: [PATCH 50/51] fix: video tab bug --- .../Container/CourseContainerViewModel.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 011a7f5ff..1c5b40012 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -1316,11 +1316,12 @@ extension CourseTab { } self.courseStructure = updatedStructure - self.courseVideosStructure = nil - let newVideoStructure = await interactor.getCourseVideoBlocks(fullStructure: updatedStructure) - self.courseVideosStructure = newVideoStructure - self.courseAssignmentsStructure = await interactor.getCourseAssignmentBlocks(fullStructure: updatedStructure) - updateAssignmentSections() + + if courseVideosStructure != nil { + let newVideoStructure = await interactor.getCourseVideoBlocks(fullStructure: updatedStructure) + self.courseVideosStructure = newVideoStructure + } + isRefreshingVideoProgress = false } From 9e3f1177fbceb8fe3b189b19f9c5d89f23b4f0c4 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 11 Feb 2026 11:29:40 +0200 Subject: [PATCH 51/51] fix: iOS 26 back button ui --- .../EditProfile/EditProfileView.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift index db11f1998..345df8462 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift @@ -226,7 +226,22 @@ public struct EditProfileView: View { BackNavigationButton(color: Theme.Colors.accentColor) { viewModel.backButtonTapped() } - .offset(x: -8, y: -1.5) + .offset( + x: { + if #available(iOS 26.0, *) { + return 6 + } else { + return -8 + } + }(), + y: { + if #available(iOS 26.0, *) { + return 1 + } else { + return -1.5 + } + }() + ) } ) ToolbarItem(placement: .navigationBarTrailing, content: {