From 17acafb1efe81ad07a18a8d92c4a197c313b3169 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Mon, 25 Mar 2024 16:37:14 +0300 Subject: [PATCH 01/20] feat: pip mode --- Course/Course.xcodeproj/project.pbxproj | 4 + .../Unit/CourseUnitViewModel.swift | 2 +- .../Video/EncodedVideoPlayer.swift | 8 +- .../Video/EncodedVideoPlayerViewModel.swift | 39 +++++- .../Video/PlayerViewController.swift | 30 ++++- .../Video/PlayerViewControllerHolder.swift | 108 +++++++++++++++++ OpenEdX.xcodeproj/project.pbxproj | 4 + OpenEdX/DI/AppAssembly.swift | 12 ++ OpenEdX/DI/ScreenAssembly.swift | 9 +- OpenEdX/Info.plist | 4 + .../DeepLinkRouter/DeepLinkRouter.swift | 4 + OpenEdX/Managers/PipManager.swift | 113 ++++++++++++++++++ 12 files changed, 320 insertions(+), 17 deletions(-) create mode 100644 Course/Course/Presentation/Video/PlayerViewControllerHolder.swift create mode 100644 OpenEdX/Managers/PipManager.swift diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index 9de40f83f..38647a0b8 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ 02F98A8128F8224200DE94C0 /* Discussion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02F98A8028F8224200DE94C0 /* Discussion.framework */; }; 02FFAD0D29E4347300140E46 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FFAD0C29E4347300140E46 /* VideoPlayerViewModel.swift */; }; 060E8BCA2B5FD68C0080C952 /* UnitStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060E8BC92B5FD68C0080C952 /* UnitStack.swift */; }; + 065275352BB1B39C0093BCCA /* PlayerViewControllerHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065275342BB1B39C0093BCCA /* PlayerViewControllerHolder.swift */; }; 068DDA5F2B1E198700FF8CCB /* CourseUnitDropDownList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068DDA5B2B1E198700FF8CCB /* CourseUnitDropDownList.swift */; }; 068DDA602B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068DDA5C2B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift */; }; 068DDA612B1E198700FF8CCB /* CourseUnitDropDownCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068DDA5D2B1E198700FF8CCB /* CourseUnitDropDownCell.swift */; }; @@ -148,6 +149,7 @@ 02F98A8028F8224200DE94C0 /* Discussion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discussion.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 02FFAD0C29E4347300140E46 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; 060E8BC92B5FD68C0080C952 /* UnitStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitStack.swift; sourceTree = ""; }; + 065275342BB1B39C0093BCCA /* PlayerViewControllerHolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerViewControllerHolder.swift; sourceTree = ""; }; 068DDA5B2B1E198700FF8CCB /* CourseUnitDropDownList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseUnitDropDownList.swift; sourceTree = ""; }; 068DDA5C2B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseUnitVerticalsDropdownView.swift; sourceTree = ""; }; 068DDA5D2B1E198700FF8CCB /* CourseUnitDropDownCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseUnitDropDownCell.swift; sourceTree = ""; }; @@ -440,6 +442,7 @@ 070019AA28F6F79E00D5FC78 /* Video */ = { isa = PBXGroup; children = ( + 065275342BB1B39C0093BCCA /* PlayerViewControllerHolder.swift */, 02F066E729DC71750073E13B /* SubtittlesView.swift */, 0766DFCB299AA7A600EBEF6A /* YouTubeVideoPlayer.swift */, 022F8E152A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift */, @@ -816,6 +819,7 @@ 022C64E029ADEA9B000F532B /* Data_UpdatesResponse.swift in Sources */, 02454CA02A2618E70043052A /* YouTubeView.swift in Sources */, 02454CA22A26190A0043052A /* EncodedVideoView.swift in Sources */, + 065275352BB1B39C0093BCCA /* PlayerViewControllerHolder.swift in Sources */, 068DDA612B1E198700FF8CCB /* CourseUnitDropDownCell.swift in Sources */, BAC0E0DE2B32F0F3006B68A9 /* DownloadsViewModel.swift in Sources */, 02B6B3BC28E1D14F00232911 /* CourseRepository.swift in Sources */, diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index 586ef427a..d272d3679 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -142,7 +142,7 @@ public class CourseUnitViewModel: ObservableObject { private func selectLesson() -> Int { guard verticals[verticalIndex].childs.count > 0 else { return 0 } - let index = verticals[verticalIndex].childs.firstIndex(where: { $0.id == lessonID }) ?? 0 + let index = verticals[verticalIndex].childs.firstIndex(where: { $0.id.contains(lessonID) }) ?? 0 nextTitles() return index } diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index 6249ccb7b..af27fd4ac 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -59,7 +59,7 @@ public struct EncodedVideoPlayer: View { VStack { PlayerViewController( videoURL: viewModel.url, - controller: viewModel.controller, + playerHolder: viewModel.controllerHolder, bitrate: viewModel.getVideoResolution(), progress: { progress in if progress >= 0.8 { @@ -149,9 +149,11 @@ struct EncodedVideoPlayer_Previews: PreviewProvider { languages: [], playerStateSubject: CurrentValueSubject(nil), interactor: CourseInteractor(repository: CourseRepositoryMock()), - router: CourseRouterMock(), + router: CourseRouterMock(), appStorage: CoreStorageMock(), - connectivity: Connectivity() + connectivity: Connectivity(), + pipManager: PipManagerProtocolMock(), + isVideoTab: false ), isOnScreen: true ) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift index 6163c8f93..0f6ba0cca 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift @@ -8,12 +8,16 @@ import _AVKit_SwiftUI import Core import Combine +import Swinject public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { let url: URL? - let controller = AVPlayerViewController() + let controllerHolder: PlayerViewControllerHolder + var controller: AVPlayerViewController { + controllerHolder.playerController + } private var subscription = Set() public init( @@ -25,24 +29,49 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { interactor: CourseInteractorProtocol, router: CourseRouter, appStorage: CoreStorage, - connectivity: ConnectivityProtocol + connectivity: ConnectivityProtocol, + pipManager: PipManagerProtocol, + isVideoTab: Bool ) { self.url = url + if let holder = pipManager.holder( + for: url, + blockID: blockID, + courseID: courseID, + isVideoTab: isVideoTab + ) { + print("ALARM restore holder") + controllerHolder = holder + } else { + print("ALARM create holder") + let holder = PlayerViewControllerHolder( + url: url, + blockID: blockID, + courseID: courseID, + isVideoTab: isVideoTab + ) + controllerHolder = holder + } + super.init(blockID: blockID, courseID: courseID, languages: languages, interactor: interactor, - router: router, + router: router, appStorage: appStorage, connectivity: connectivity) playerStateSubject.sink(receiveValue: { [weak self] state in switch state { case .pause: - self?.controller.player?.pause() + if self?.controllerHolder.isPipModeActive != true { + self?.controller.player?.pause() + } case .kill: - self?.controller.player?.replaceCurrentItem(with: nil) + if self?.controllerHolder.isPipModeActive != true { + self?.controller.player?.replaceCurrentItem(with: nil) + } case .none: break } diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift index 40938029f..92e30b662 100644 --- a/Course/Course/Presentation/Video/PlayerViewController.swift +++ b/Course/Course/Presentation/Video/PlayerViewController.swift @@ -5,6 +5,8 @@ // Created by Vladimir Chekyrta on 13.02.2023. // +import Combine +import Core import SwiftUI import _AVKit_SwiftUI @@ -12,27 +14,34 @@ struct PlayerViewController: UIViewControllerRepresentable { var videoURL: URL? var videoResolution: CGSize - var controller: AVPlayerViewController + var playerHolder: PlayerViewControllerHolder var progress: ((Float) -> Void) var seconds: ((Double) -> Void) init( videoURL: URL?, - controller: AVPlayerViewController, + playerHolder: PlayerViewControllerHolder, bitrate: CGSize, progress: @escaping ((Float) -> Void), seconds: @escaping ((Double) -> Void) ) { self.videoURL = videoURL - self.controller = controller + self.playerHolder = playerHolder self.videoResolution = bitrate self.progress = progress self.seconds = seconds } func makeUIViewController(context: Context) -> AVPlayerViewController { + if playerHolder.isPipModeActive { + return playerHolder.playerController + } + + print("ALARM create new player") + let controller = playerHolder.playerController controller.modalPresentationStyle = .fullScreen controller.allowsPictureInPicturePlayback = true + controller.canStartPictureInPictureAutomaticallyFromInline = true let player = AVPlayer() controller.player = player context.coordinator.setPlayer(player) { progress, seconds in @@ -51,7 +60,8 @@ struct PlayerViewController: UIViewControllerRepresentable { func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) { let asset = playerController.player?.currentItem?.asset as? AVURLAsset - if asset?.url.absoluteString != videoURL?.absoluteString { + if asset?.url.absoluteString != videoURL?.absoluteString && !playerHolder.isPipModeActive { + print("ALARM replace player") let player = context.coordinator.player(from: playerController) player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL!)) player?.currentItem?.preferredMaximumResolution = videoResolution @@ -74,6 +84,7 @@ struct PlayerViewController: UIViewControllerRepresentable { class Coordinator { var currentPlayer: AVPlayer? var observer: Any? + var cancellations: [AnyCancellable] = [] func player(from playerController: AVPlayerViewController) -> AVPlayer? { var player = playerController.player @@ -86,6 +97,8 @@ struct PlayerViewController: UIViewControllerRepresentable { } func setPlayer(_ player: AVPlayer?, currentProgress: @escaping ((Float, Double) -> Void)) { + guard let player = player else { return } + cancellations.removeAll() if let observer = observer { currentPlayer?.removeTimeObserver(observer) currentPlayer?.pause() @@ -96,7 +109,7 @@ struct PlayerViewController: UIViewControllerRepresentable { preferredTimescale: CMTimeScale(NSEC_PER_SEC) ) - observer = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) {[weak player] time in + observer = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) {[weak player] time in var progress: Float = .zero let currentSeconds = CMTimeGetSeconds(time) guard let duration = player?.currentItem?.duration else { return } @@ -105,6 +118,13 @@ struct PlayerViewController: UIViewControllerRepresentable { currentProgress(progress, currentSeconds) } + player.publisher(for: \.rate) + .sink { rate in + guard rate > 0 else { return } + + } + .store(in: &cancellations) + currentPlayer = player } diff --git a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift new file mode 100644 index 000000000..2393d7e82 --- /dev/null +++ b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift @@ -0,0 +1,108 @@ +// +// PlayerViewControllerHolder.swift +// Core +// +// Created by Vadim Kuznetsov on 20.03.24. +// + +import AVKit +import Combine +import Swinject + +public protocol PipManagerProtocol { + func holder(for url: URL?, blockID: String, courseID: String, isVideoTab: Bool) -> PlayerViewControllerHolder? + func set(holder: PlayerViewControllerHolder) + func remove(holder: PlayerViewControllerHolder) + func restore(holder: PlayerViewControllerHolder) async throws + func appearancePublisher(for holder: PlayerViewControllerHolder) -> AnyPublisher? +} + +#if DEBUG +public class PipManagerProtocolMock: PipManagerProtocol { + public init() {} + public func holder( + for url: URL?, + blockID: String, + courseID: String, + isVideoTab: Bool + ) -> PlayerViewControllerHolder? { + return nil + } + public func set(holder: PlayerViewControllerHolder) {} + public func remove(holder: PlayerViewControllerHolder) {} + public func restore(holder: PlayerViewControllerHolder) async throws {} + public func appearancePublisher(for holder: PlayerViewControllerHolder) -> AnyPublisher? { + return nil + } +} +#endif + +public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegate { + public let url: URL? + public let blockID: String + public let courseID: String + public let isVideoTab: Bool + public var isPipModeActive: Bool = false + + public lazy var playerController: AVPlayerViewController = { + let playerController = AVPlayerViewController() + playerController.delegate = self + return playerController + }() + + public init( + url: URL?, + blockID: String, + courseID: String, + isVideoTab: Bool + ) { + self.url = url + self.blockID = blockID + self.courseID = courseID + self.isVideoTab = isVideoTab + } + + public func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { + isPipModeActive = true + Container.shared.resolve(PipManagerProtocol.self)?.set(holder: self) + } + +// func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) { +// +// } + + public func playerViewController(_ playerViewController: AVPlayerViewController, failedToStartPictureInPictureWithError error: any Error) { + isPipModeActive = false + Container.shared.resolve(PipManagerProtocol.self)?.remove(holder: self) + print("ALARM failed to start \(error)") + } + + public func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { + isPipModeActive = false + Container.shared.resolve(PipManagerProtocol.self)?.remove(holder: self) + print("ALARM did stop picture in picture") + } + +// func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { +// +// } + public func playerViewControllerRestoreUserInterfaceForPictureInPictureStop(_ playerViewController: AVPlayerViewController) async -> Bool { + print("ALARM restore controller") + do { + try await Container.shared.resolve(PipManagerProtocol.self)?.restore(holder: self) + print("ALARM restore completed") + return true + } catch { + print("ALARM restore failed") + return false + } + } + + static func == (lhs: PlayerViewControllerHolder, rhs: PlayerViewControllerHolder) -> Bool { + lhs.url?.absoluteString == rhs.url?.absoluteString && + lhs.courseID == rhs.courseID && + lhs.blockID == rhs.blockID && + lhs.isVideoTab == rhs.isVideoTab + } +} + diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 043a37fbf..625fc1c8a 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 02ED50D429A6554E008341CD /* сountries.json in Resources */ = {isa = PBXBuildFile; fileRef = 02ED50D629A6554E008341CD /* сountries.json */; }; 02ED50D829A66007008341CD /* languages.json in Resources */ = {isa = PBXBuildFile; fileRef = 02ED50DA29A66007008341CD /* languages.json */; }; 02F175312A4DA95B0019CD70 /* MainScreenAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F175302A4DA95B0019CD70 /* MainScreenAnalytics.swift */; }; + 065275372BB1B4070093BCCA /* PipManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065275362BB1B4070093BCCA /* PipManager.swift */; }; 071009C928D1DB3F00344290 /* ScreenAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 071009C828D1DB3F00344290 /* ScreenAssembly.swift */; }; 0727878E28D347C7002E9142 /* MainScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727878D28D347C7002E9142 /* MainScreenView.swift */; }; 072787B128D34D83002E9142 /* Discovery.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072787B028D34D83002E9142 /* Discovery.framework */; }; @@ -112,6 +113,7 @@ 02ED50D929A66007008341CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = Base; path = Base.lproj/languages.json; sourceTree = ""; }; 02ED50DB29A6600B008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = uk; path = uk.lproj/languages.json; sourceTree = ""; }; 02F175302A4DA95B0019CD70 /* MainScreenAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenAnalytics.swift; sourceTree = ""; }; + 065275362BB1B4070093BCCA /* PipManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PipManager.swift; sourceTree = ""; }; 071009C828D1DB3F00344290 /* ScreenAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenAssembly.swift; sourceTree = ""; }; 0727878D28D347C7002E9142 /* MainScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenView.swift; sourceTree = ""; }; 072787B028D34D83002E9142 /* Discovery.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discovery.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -292,6 +294,7 @@ A50066882B613E800024680B /* Managers */ = { isa = PBXGroup; children = ( + 065275362BB1B4070093BCCA /* PipManager.swift */, A59568932B6162E400ED4F90 /* DeepLinkManager */, A50066872B613E4B0024680B /* PushNotificationsManager */, A50066892B613E990024680B /* AnalyticsManager */, @@ -576,6 +579,7 @@ A500668D2B6143000024680B /* FCMProvider.swift in Sources */, 025AD4AC2A6FB95C00AB8FA7 /* DatabaseManager.swift in Sources */, 024E69202AEFC3FB00FA0B59 /* MainScreenViewModel.swift in Sources */, + 065275372BB1B4070093BCCA /* PipManager.swift in Sources */, 0770DE2028D0858A006D8A5D /* Router.swift in Sources */, 0293A2092A6FCDE50090A336 /* DashboardPersistence.swift in Sources */, 0770DE1728D080A1006D8A5D /* RouteController.swift in Sources */, diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 8077b4d8d..54a667cdb 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -119,6 +119,10 @@ class AppAssembly: Assembly { r.resolve(Router.self)! }.inObjectScope(.container) + container.register(DeepLinkRouter.self) { r in + r.resolve(Router.self)! + }.inObjectScope(.container) + container.register(ConfigProtocol.self) { _ in Config() }.inObjectScope(.container) @@ -193,6 +197,14 @@ class AppAssembly: Assembly { config: r.resolve(ConfigProtocol.self)! ) }.inObjectScope(.container) + + container.register(PipManagerProtocol.self) { r in + PipManager( + router: r.resolve(Router.self)!, + discoveryInteractor: r.resolve(DiscoveryInteractorProtocol.self)!, + courseInteractor: r.resolve(CourseInteractorProtocol.self)! + ) + }.inObjectScope(.container) } } // swiftlint:enable function_body_length diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 74074812d..414db26f2 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -327,16 +327,19 @@ class ScreenAssembly: Assembly { container.register( EncodedVideoPlayerViewModel.self ) { r, url, blockID, courseID, languages, playerStateSubject in - EncodedVideoPlayerViewModel( + let router: Router = r.resolve(Router.self)! + return EncodedVideoPlayerViewModel( url: url, blockID: blockID, courseID: courseID, languages: languages, playerStateSubject: playerStateSubject, interactor: r.resolve(CourseInteractorProtocol.self)!, - router: r.resolve(CourseRouter.self)!, + router: r.resolve(CourseRouter.self)!, appStorage: r.resolve(CoreStorage.self)!, - connectivity: r.resolve(ConnectivityProtocol.self)! + connectivity: r.resolve(ConnectivityProtocol.self)!, + pipManager: r.resolve(PipManagerProtocol.self)!, + isVideoTab: router.isVideoTab ) } diff --git a/OpenEdX/Info.plist b/OpenEdX/Info.plist index dc623f961..06a6f9e11 100644 --- a/OpenEdX/Info.plist +++ b/OpenEdX/Info.plist @@ -29,6 +29,10 @@ UIAppFonts + UIBackgroundModes + + audio + UIViewControllerBasedStatusBarAppearance diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift index 48a37bd20..83109d73f 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift @@ -58,6 +58,10 @@ extension Router: DeepLinkRouter { // MARK: - DeepLinkRouter + public var isVideoTab: Bool { + self.hostCourseContainerView?.rootView.viewModel.selection == CourseTab.videos.rawValue + } + public func showDiscoveryDetails( link: DeepLink, pathID: String diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift new file mode 100644 index 000000000..cdcd575d5 --- /dev/null +++ b/OpenEdX/Managers/PipManager.swift @@ -0,0 +1,113 @@ +// +// PipManager.swift +// OpenEdX +// +// Created by Vadim Kuznetsov on 20.03.24. +// + +import Combine +import Course +import Discovery +import Foundation + +public class PipManager: PipManagerProtocol { + var controllerHolder: PlayerViewControllerHolder? + private var appearancePublisher = PassthroughSubject() + private var restorationTask: Task? + private var cancellations: [AnyCancellable] = [] + let discoveryInteractor: DiscoveryInteractorProtocol + let courseInteractor: CourseInteractorProtocol + let router: Router + public init( + router: Router, + discoveryInteractor: DiscoveryInteractorProtocol, + courseInteractor: CourseInteractorProtocol + ) { + self.discoveryInteractor = discoveryInteractor + self.courseInteractor = courseInteractor + self.router = router + } + + public func holder( + for url: URL?, + blockID: String, + courseID: String, + isVideoTab: Bool + ) -> PlayerViewControllerHolder? { + if controllerHolder?.blockID == blockID, + controllerHolder?.courseID == courseID, + controllerHolder?.isVideoTab == isVideoTab { + return controllerHolder + } + + return nil + } + + public func set(holder: PlayerViewControllerHolder) { + controllerHolder = holder + appearancePublisher = PassthroughSubject() + cancellations.removeAll() + restorationTask?.cancel() + restorationTask = nil + } + + public func remove(holder: PlayerViewControllerHolder) { + if controllerHolder == holder { + controllerHolder = nil + restorationTask?.cancel() + restorationTask = nil + } + } + + @MainActor + public func restore(holder: PlayerViewControllerHolder) async throws { + let courseID = holder.courseID + var courseDetails: CourseDetails? + + if let value = try? await discoveryInteractor.getLoadedCourseDetails( + courseID: courseID + ) { + courseDetails = value + } else { + courseDetails = try await discoveryInteractor.getCourseDetails( + courseID: courseID + ) + } + guard let courseDetails = courseDetails else { throw PipManagerError.cantGetCourseDetails } + let link = DeepLink(dictionary: [:]) + link.type = holder.isVideoTab ? .courseVideos : .courseDashboard + await showCourseDetail(link: link, courseDetails: courseDetails) + + var courseStructure = try await courseInteractor.getLoadedCourseBlocks(courseID: courseID) + if holder.isVideoTab { + courseStructure = courseInteractor.getCourseVideoBlocks(fullStructure: courseStructure) + } + router.showCourseComponent(componentID: holder.blockID, courseStructure: courseStructure) + } + + @MainActor + func showCourseDetail(link: DeepLink, courseDetails: CourseDetails) async { + await withCheckedContinuation { continuation in + router.showCourseDetail( + link: link, + courseDetails: courseDetails + ) { + continuation.resume() + } + } + } + + public func appearancePublisher(for holder: Course.PlayerViewControllerHolder) -> AnyPublisher? { + if holder == controllerHolder { + return appearancePublisher + .eraseToAnyPublisher() + } + return nil + } +} + +extension PipManager { + enum PipManagerError: Error { + case cantGetCourseDetails + } +} From cf443565c8df43395b1fca69360af17422007d68 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Thu, 28 Mar 2024 17:58:15 +0300 Subject: [PATCH 02/20] fix: pip restoration --- Course/Course/Presentation/CourseRouter.swift | 4 - .../Outline/CourseOutlineView.swift | 1 - .../CourseStructureNestedListView.swift | 1 - .../CourseVertical/CourseVerticalView.swift | 1 - .../Unit/CourseNavigationView.swift | 5 - .../Presentation/Unit/CourseUnitView.swift | 11 +- .../Unit/CourseUnitViewModel.swift | 64 ++++- .../Video/EncodedVideoPlayer.swift | 2 +- .../Video/EncodedVideoPlayerViewModel.swift | 6 +- .../Video/PlayerViewControllerHolder.swift | 43 +-- OpenEdX/DI/ScreenAssembly.swift | 2 +- .../DeepLinkRouter/DeepLinkRouter.swift | 8 +- OpenEdX/Managers/PipManager.swift | 258 +++++++++++++++--- OpenEdX/Router.swift | 11 +- 14 files changed, 313 insertions(+), 104 deletions(-) diff --git a/Course/Course/Presentation/CourseRouter.swift b/Course/Course/Presentation/CourseRouter.swift index d4cd7c68a..50b2cdef1 100644 --- a/Course/Course/Presentation/CourseRouter.swift +++ b/Course/Course/Presentation/CourseRouter.swift @@ -16,7 +16,6 @@ public protocol CourseRouter: BaseRouter { courseName: String, blockId: String, courseID: String, - sectionName: String, verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, @@ -27,7 +26,6 @@ public protocol CourseRouter: BaseRouter { courseName: String, blockId: String, courseID: String, - sectionName: String, verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, @@ -74,7 +72,6 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { courseName: String, blockId: String, courseID: String, - sectionName: String, verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, @@ -85,7 +82,6 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { courseName: String, blockId: String, courseID: String, - sectionName: String, verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index b4ebb132e..227be89ed 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -105,7 +105,6 @@ public struct CourseOutlineView: View { courseName: course.displayName, blockId: continueBlock?.id ?? "", courseID: course.id, - sectionName: continueUnit.displayName, verticalIndex: continueWith.verticalIndex, chapters: course.childs, chapterIndex: continueWith.chapterIndex, diff --git a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift index e057b3b7c..4e5e853df 100644 --- a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift +++ b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift @@ -224,7 +224,6 @@ struct CourseStructureNestedListView: View { courseName: viewModel.courseStructure?.displayName ?? "", blockId: block.id, courseID: viewModel.courseStructure?.id ?? "", - sectionName: block.displayName, verticalIndex: 0, chapters: course.childs, chapterIndex: chapterIndex, diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift index 889d6e155..2cbdc7e9e 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift @@ -54,7 +54,6 @@ public struct CourseVerticalView: View { courseName: courseName, blockId: block.id, courseID: courseID, - sectionName: block.displayName, verticalIndex: index, chapters: viewModel.chapters, chapterIndex: viewModel.chapterIndex, diff --git a/Course/Course/Presentation/Unit/CourseNavigationView.swift b/Course/Course/Presentation/Unit/CourseNavigationView.swift index 0834019e3..0dea684b8 100644 --- a/Course/Course/Presentation/Unit/CourseNavigationView.swift +++ b/Course/Course/Presentation/Unit/CourseNavigationView.swift @@ -13,16 +13,13 @@ struct CourseNavigationView: View { @ObservedObject private var viewModel: CourseUnitViewModel - private let sectionName: String private let playerStateSubject: CurrentValueSubject init( - sectionName: String, viewModel: CourseUnitViewModel, playerStateSubject: CurrentValueSubject ) { self.viewModel = viewModel - self.sectionName = sectionName self.playerStateSubject = playerStateSubject } @@ -129,7 +126,6 @@ struct CourseNavigationView: View { courseName: viewModel.courseName, blockId: viewModel.lessonID, courseID: viewModel.courseID, - sectionName: viewModel.selectedLesson().displayName, verticalIndex: data.verticalIndex, chapters: viewModel.chapters, chapterIndex: data.chapterIndex, @@ -171,7 +167,6 @@ struct CourseNavigationView_Previews: PreviewProvider { ) CourseNavigationView( - sectionName: "Name", viewModel: viewModel, playerStateSubject: CurrentValueSubject(nil) ) diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 73b529d32..1219cea91 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -14,7 +14,7 @@ import Theme public struct CourseUnitView: View { - @ObservedObject private var viewModel: CourseUnitViewModel + @ObservedObject public var viewModel: CourseUnitViewModel @State private var showAlert: Bool = false @State var alertMessage: String? { didSet { @@ -27,7 +27,6 @@ public struct CourseUnitView: View { @State var showDiscussion: Bool = false @Environment(\.isPresented) private var isPresented @Environment(\.isHorizontal) private var isHorizontal - private let sectionName: String public let playerStateSubject = CurrentValueSubject(nil) //Dropdown parameters @@ -60,11 +59,9 @@ public struct CourseUnitView: View { public init( viewModel: CourseUnitViewModel, - sectionName: String, isDropdownActive: Bool = false ) { self.viewModel = viewModel - self.sectionName = sectionName self.isDropdownActive = isDropdownActive viewModel.loadIndex() viewModel.nextTitles() @@ -122,7 +119,8 @@ public struct CourseUnitView: View { offsetY: isHorizontal ? landscapeTopSpacing : portraitTopSpacing, showDropdown: $showDropdown ) { [weak viewModel] vertical in - viewModel?.route(to: vertical) + let data = viewModel?.dataFor(blockId: vertical.childs.first?.id) + viewModel?.route(to: data) } } } @@ -413,7 +411,6 @@ public struct CourseUnitView: View { Spacer() } CourseNavigationView( - sectionName: sectionName, viewModel: viewModel, playerStateSubject: playerStateSubject ) @@ -558,7 +555,7 @@ struct CourseUnitView_Previews: PreviewProvider { connectivity: Connectivity(), storage: CourseStorageMock(), manager: DownloadManagerMock() - ), sectionName: "") + )) } } //swiftlint:enable all diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index d272d3679..ca18d3250 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -31,7 +31,8 @@ public enum LessonType: Equatable { case .discussion: return .discussion(block.topicId ?? "", block.id, block.displayName) case .video: - if block.encodedVideo?.youtubeVideoUrl != nil, let encodedVideo = block.encodedVideo?.video(streamingQuality: streamingQuality)?.url { + if block.encodedVideo?.youtubeVideoUrl != nil, + let encodedVideo = block.encodedVideo?.video(streamingQuality: streamingQuality)?.url { return .video(videoUrl: encodedVideo, blockID: block.id) } else if let youtubeVideoUrl = block.encodedVideo?.youtubeVideoUrl { return .youtube(youtubeVideoUrl: youtubeVideoUrl, blockID: block.id) @@ -64,6 +65,7 @@ public class CourseUnitViewModel: ObservableObject { var chapterIndex: Int var sequentialIndex: Int var verticalIndex: Int + var blockIndex: Int } var verticals: [CourseVertical] @@ -95,7 +97,7 @@ public class CourseUnitViewModel: ObservableObject { let chapterIndex: Int let sequentialIndex: Int - var streamingQuality: StreamingQuality { + var streamingQuality: StreamingQuality { storage.userSettings?.streamingQuality ?? .auto } @@ -221,7 +223,8 @@ public class CourseUnitViewModel: ObservableObject { from: VerticalData( chapterIndex: chapterIndex, sequentialIndex: sequentialIndex, - verticalIndex: verticalIndex + verticalIndex: verticalIndex, + blockIndex: 0 ) ) } @@ -271,6 +274,7 @@ public class CourseUnitViewModel: ObservableObject { } if let vertical = vertical(for: resultData), vertical.childs.count > 0 { + resultData.blockIndex = 0 return resultData } else { return nextData(from: resultData) @@ -291,20 +295,58 @@ public class CourseUnitViewModel: ObservableObject { ) } - func route(to vertical: CourseVertical) { - if let index = verticals.firstIndex(where: { $0.id == vertical.id }), - let block = vertical.childs.first { + func blockFor(index: Int, in vertical: CourseVertical) -> CourseBlock? { + guard index >= 0 && index < vertical.childs.count else { return nil } + return vertical.childs[index] + } + + func route(to data: VerticalData?, animated: Bool = false) { + guard let data = data else { return } + if data.verticalIndex == verticalIndex, + let block = blockFor(index: data.blockIndex, in: verticals[verticalIndex]) { + // if we are on same vertical now + lessonID = block.blockId + loadIndex() + } else if let vertical = vertical(for: data), + let block = blockFor(index: data.blockIndex, in: vertical) { router.replaceCourseUnit( courseName: courseName, blockId: block.id, courseID: courseID, - sectionName: block.displayName, - verticalIndex: index, + verticalIndex: data.verticalIndex, chapters: chapters, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex, - animated: false + chapterIndex: data.chapterIndex, + sequentialIndex: data.sequentialIndex, + animated: animated ) } } + + public func route(to blockId: String?) { + guard let data = dataFor(blockId: blockId) else { return } + route(to: data, animated: true) + } + + func dataFor(blockId: String?) -> VerticalData? { + guard let blockId = blockId else { return nil } + for (chapterIndex, chapter) in chapters.enumerated() { + for (sequentialIndex, sequential) in chapter.childs.enumerated() { + for (verticalIndex, vertical) in sequential.childs.enumerated() { + for (blockIndex, block) in vertical.childs.enumerated() where block.id.contains(blockId) { + return VerticalData( + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex, + verticalIndex: verticalIndex, + blockIndex: blockIndex + ) + } + } + } + } + return nil + } + + public var currentCourseId: String { + courseID + } } diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index af27fd4ac..c3f64dc17 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -153,7 +153,7 @@ struct EncodedVideoPlayer_Previews: PreviewProvider { appStorage: CoreStorageMock(), connectivity: Connectivity(), pipManager: PipManagerProtocolMock(), - isVideoTab: false + selectedCourseTab: 0 ), isOnScreen: true ) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift index 0f6ba0cca..5f4121c2c 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift @@ -31,7 +31,7 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { appStorage: CoreStorage, connectivity: ConnectivityProtocol, pipManager: PipManagerProtocol, - isVideoTab: Bool + selectedCourseTab: Int ) { self.url = url @@ -39,7 +39,7 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { for: url, blockID: blockID, courseID: courseID, - isVideoTab: isVideoTab + selectedCourseTab: selectedCourseTab ) { print("ALARM restore holder") controllerHolder = holder @@ -49,7 +49,7 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { url: url, blockID: blockID, courseID: courseID, - isVideoTab: isVideoTab + selectedCourseTab: selectedCourseTab ) controllerHolder = holder } diff --git a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift index 2393d7e82..c724995ca 100644 --- a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift +++ b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift @@ -10,11 +10,10 @@ import Combine import Swinject public protocol PipManagerProtocol { - func holder(for url: URL?, blockID: String, courseID: String, isVideoTab: Bool) -> PlayerViewControllerHolder? + func holder(for url: URL?, blockID: String, courseID: String, selectedCourseTab: Int) -> PlayerViewControllerHolder? func set(holder: PlayerViewControllerHolder) func remove(holder: PlayerViewControllerHolder) func restore(holder: PlayerViewControllerHolder) async throws - func appearancePublisher(for holder: PlayerViewControllerHolder) -> AnyPublisher? } #if DEBUG @@ -24,16 +23,13 @@ public class PipManagerProtocolMock: PipManagerProtocol { for url: URL?, blockID: String, courseID: String, - isVideoTab: Bool + selectedCourseTab: Int ) -> PlayerViewControllerHolder? { return nil } public func set(holder: PlayerViewControllerHolder) {} public func remove(holder: PlayerViewControllerHolder) {} public func restore(holder: PlayerViewControllerHolder) async throws {} - public func appearancePublisher(for holder: PlayerViewControllerHolder) -> AnyPublisher? { - return nil - } } #endif @@ -41,7 +37,7 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat public let url: URL? public let blockID: String public let courseID: String - public let isVideoTab: Bool + public let selectedCourseTab: Int public var isPipModeActive: Bool = false public lazy var playerController: AVPlayerViewController = { @@ -54,24 +50,27 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat url: URL?, blockID: String, courseID: String, - isVideoTab: Bool + selectedCourseTab: Int ) { self.url = url self.blockID = blockID self.courseID = courseID - self.isVideoTab = isVideoTab + self.selectedCourseTab = selectedCourseTab } - + public func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { isPipModeActive = true Container.shared.resolve(PipManagerProtocol.self)?.set(holder: self) } - + // func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) { // // } - - public func playerViewController(_ playerViewController: AVPlayerViewController, failedToStartPictureInPictureWithError error: any Error) { + + public func playerViewController( + _ playerViewController: AVPlayerViewController, + failedToStartPictureInPictureWithError error: any Error + ) { isPipModeActive = false Container.shared.resolve(PipManagerProtocol.self)?.remove(holder: self) print("ALARM failed to start \(error)") @@ -86,7 +85,9 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat // func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { // // } - public func playerViewControllerRestoreUserInterfaceForPictureInPictureStop(_ playerViewController: AVPlayerViewController) async -> Bool { + public func playerViewControllerRestoreUserInterfaceForPictureInPictureStop( + _ playerViewController: AVPlayerViewController + ) async -> Bool { print("ALARM restore controller") do { try await Container.shared.resolve(PipManagerProtocol.self)?.restore(holder: self) @@ -98,11 +99,13 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat } } - static func == (lhs: PlayerViewControllerHolder, rhs: PlayerViewControllerHolder) -> Bool { - lhs.url?.absoluteString == rhs.url?.absoluteString && - lhs.courseID == rhs.courseID && - lhs.blockID == rhs.blockID && - lhs.isVideoTab == rhs.isVideoTab + public override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? PlayerViewControllerHolder else { + return false + } + return url?.absoluteString == object.url?.absoluteString && + courseID == object.courseID && + blockID == object.blockID && + selectedCourseTab == object.selectedCourseTab } } - diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 414db26f2..7a17c656d 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -339,7 +339,7 @@ class ScreenAssembly: Assembly { appStorage: r.resolve(CoreStorage.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, pipManager: r.resolve(PipManagerProtocol.self)!, - isVideoTab: router.isVideoTab + selectedCourseTab: router.currentCourseTabSelection ) } diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift index 83109d73f..e2515d36d 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift @@ -57,11 +57,6 @@ public protocol DeepLinkRouter: BaseRouter { extension Router: DeepLinkRouter { // MARK: - DeepLinkRouter - - public var isVideoTab: Bool { - self.hostCourseContainerView?.rootView.viewModel.selection == CourseTab.videos.rawValue - } - public func showDiscoveryDetails( link: DeepLink, pathID: String @@ -313,6 +308,9 @@ extension Router: DeepLinkRouter { backToRoot(animated: false) } + public var currentCourseTabSelection: Int { + self.hostCourseContainerView?.rootView.viewModel.selection ?? 0 + } } // Mark - For testing and SwiftUI preview diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index cdcd575d5..d25095766 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -9,12 +9,13 @@ import Combine import Course import Discovery import Foundation +import SwiftUI +import Swinject +import Core public class PipManager: PipManagerProtocol { var controllerHolder: PlayerViewControllerHolder? - private var appearancePublisher = PassthroughSubject() private var restorationTask: Task? - private var cancellations: [AnyCancellable] = [] let discoveryInteractor: DiscoveryInteractorProtocol let courseInteractor: CourseInteractorProtocol let router: Router @@ -27,26 +28,25 @@ public class PipManager: PipManagerProtocol { self.courseInteractor = courseInteractor self.router = router } - + public func holder( for url: URL?, blockID: String, courseID: String, - isVideoTab: Bool + selectedCourseTab: Int ) -> PlayerViewControllerHolder? { + print("ALARM navigationStack: \(router.getNavigationController().children)") if controllerHolder?.blockID == blockID, controllerHolder?.courseID == courseID, - controllerHolder?.isVideoTab == isVideoTab { + controllerHolder?.selectedCourseTab == selectedCourseTab { return controllerHolder } - + return nil } public func set(holder: PlayerViewControllerHolder) { controllerHolder = holder - appearancePublisher = PassthroughSubject() - cancellations.removeAll() restorationTask?.cancel() restorationTask = nil } @@ -62,52 +62,236 @@ public class PipManager: PipManagerProtocol { @MainActor public func restore(holder: PlayerViewControllerHolder) async throws { let courseID = holder.courseID - var courseDetails: CourseDetails? - if let value = try? await discoveryInteractor.getLoadedCourseDetails( - courseID: courseID - ) { - courseDetails = value + // if we are on CourseUnitView, and tab is same with holder + if let controller = topCourseUnitController, + router.currentCourseTabSelection == holder.selectedCourseTab { + let viewModel = controller.rootView.viewModel + + if viewModel.currentCourseId == courseID { + viewModel.route(to: holder.blockID) + return + } + } + + + try await navigate(to: holder) + } + + @MainActor + func navigate(to holder: PlayerViewControllerHolder) async throws { + let currentControllers = router.getNavigationController().viewControllers + guard let mainController = currentControllers.first as? UIHostingController else { + return + } + + mainController.rootView.viewModel.select(tab: .dashboard) + + var viewControllers: [UIViewController] = [mainController] + if currentControllers.count > 1, + let containerController = currentControllers[1] as? UIHostingController, + containerController.rootView.courseID == holder.courseID { + containerController.rootView.viewModel.selection = holder.selectedCourseTab + viewControllers.append(containerController) } else { - courseDetails = try await discoveryInteractor.getCourseDetails( - courseID: courseID - ) + viewControllers.append(try await containerController(for: holder)) } - guard let courseDetails = courseDetails else { throw PipManagerError.cantGetCourseDetails } - let link = DeepLink(dictionary: [:]) - link.type = holder.isVideoTab ? .courseVideos : .courseDashboard - await showCourseDetail(link: link, courseDetails: courseDetails) - var courseStructure = try await courseInteractor.getLoadedCourseBlocks(courseID: courseID) - if holder.isVideoTab { + viewControllers.append(try await courseUnitController(for: holder)) + + router.getNavigationController().setViewControllers(viewControllers, animated: true) + } + + @MainActor func courseUnitController( + for holder: PlayerViewControllerHolder + ) async throws -> UIHostingController { + + var courseStructure = try await courseInteractor.getLoadedCourseBlocks(courseID: holder.courseID) + if holder.selectedCourseTab == CourseTab.videos.rawValue { courseStructure = courseInteractor.getCourseVideoBlocks(fullStructure: courseStructure) } - router.showCourseComponent(componentID: holder.blockID, courseStructure: courseStructure) + for (chapterIndex, chapter) in courseStructure.childs.enumerated() { + for (sequentialIndex, sequential) in chapter.childs.enumerated() { + for (verticalIndex, vertical) in sequential.childs.enumerated() { + for (_, block) in vertical.childs.enumerated() where block.id == holder.blockID { + let viewModel = Container.shared.resolve( + CourseUnitViewModel.self, + arguments: block.blockId, + courseStructure.id, + courseStructure.displayName, + courseStructure.childs, + chapterIndex, + sequentialIndex, + verticalIndex + )! + + let config = Container.shared.resolve(ConfigProtocol.self) + let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false + + let view = CourseUnitView(viewModel: viewModel, isDropdownActive: isDropdownActive) + return UIHostingController(rootView: view) + } + } + } + } + + throw PipManagerError.cantCreateCourseUnitView } @MainActor - func showCourseDetail(link: DeepLink, courseDetails: CourseDetails) async { - await withCheckedContinuation { continuation in - router.showCourseDetail( - link: link, - courseDetails: courseDetails - ) { - continuation.resume() - } - } + func containerController( + for holder: PlayerViewControllerHolder + ) async throws -> UIHostingController { + let courseDetails = try await getCourseDetails(for: holder) + let isActive: Bool? = nil + + let vm = Container.shared.resolve( + CourseContainerViewModel.self, + arguments: isActive, + courseDetails.courseStart, + courseDetails.courseEnd, + courseDetails.enrollmentStart, + courseDetails.enrollmentEnd + )! + let screensView = CourseContainerView( + viewModel: vm, + courseID: courseDetails.courseID, + title: courseDetails.courseTitle + ) + + let controller = UIHostingController(rootView: screensView) + controller.rootView.viewModel.selection = holder.selectedCourseTab + return controller } - public func appearancePublisher(for holder: Course.PlayerViewControllerHolder) -> AnyPublisher? { - if holder == controllerHolder { - return appearancePublisher - .eraseToAnyPublisher() + func getCourseDetails(for holder: PlayerViewControllerHolder) async throws -> CourseDetails { + if let value = try? await discoveryInteractor.getLoadedCourseDetails( + courseID: holder.courseID + ) { + return value + } else { + return try await discoveryInteractor.getCourseDetails( + courseID: holder.courseID + ) } - return nil + } + /* + let isCourseOpened = hostCourseContainerView?.rootView.courseID == courseDetails.courseID + + if !isCourseOpened { + showTabScreen(tab: .dashboard) + + if courseDetails.isEnrolled { + showCourseScreens( + courseID: courseDetails.courseID, + isActive: nil, + courseStart: courseDetails.courseStart, + courseEnd: courseDetails.courseEnd, + enrollmentStart: courseDetails.enrollmentStart, + enrollmentEnd: courseDetails.enrollmentEnd, + title: courseDetails.courseTitle + ) + } else { + showCourseDetais( + courseID: courseDetails.courseID, + title: courseDetails.courseTitle + ) + } + } + + switch link.type { + case .courseVideos, + .courseDates, + .discussions, + .courseHandout, + .courseAnnouncement, + .courseDashboard: + popToCourseContainerView(animated: false) + default: + break + } + + DispatchQueue.main.asyncAfter(deadline: .now() + (isCourseOpened ? 0 : 1)) { + switch link.type { + case .courseDashboard: + self.hostCourseContainerView?.rootView.viewModel.selection = CourseTab.course.rawValue + case .courseVideos: + self.hostCourseContainerView?.rootView.viewModel.selection = CourseTab.videos.rawValue + case .courseDates: + self.hostCourseContainerView?.rootView.viewModel.selection = CourseTab.dates.rawValue + case .discussions, .discussionTopic, .discussionPost, .discussionComment: + self.hostCourseContainerView?.rootView.viewModel.selection = CourseTab.discussion.rawValue + case .courseHandout, .courseAnnouncement: + self.hostCourseContainerView?.rootView.viewModel.selection = CourseTab.handounds.rawValue + default: + break + } + + completion() + } + + public func showCourseScreens( + courseID: String, + isActive: Bool?, + courseStart: Date?, + courseEnd: Date?, + enrollmentStart: Date?, + enrollmentEnd: Date?, + title: String + ) { + let vm = Container.shared.resolve( + CourseContainerViewModel.self, + arguments: isActive, + courseStart, + courseEnd, + enrollmentStart, + enrollmentEnd + )! + let screensView = CourseContainerView( + viewModel: vm, + courseID: courseID, + title: title + ) + + let controller = UIHostingController(rootView: screensView) + navigationController.pushViewController(controller, animated: true) + } + + */ + + private var topCourseUnitController: UIHostingController? { + router.getNavigationController().visibleViewController as? UIHostingController } } extension PipManager { enum PipManagerError: Error { case cantGetCourseDetails + case cantCreateCourseUnitView + } +} + +extension UIViewController { + public var mostTopController: UIViewController? { + topController(from: self) + } + + private func topController(from controller: UIViewController?) -> UIViewController? { + if let navigationController = controller as? UINavigationController { + return topController(from: navigationController.visibleViewController) + } else if let tabBarController = controller as? UITabBarController { + return topController(from: tabBarController.selectedViewController) + } else if let splitController = controller as? UISplitViewController { + return topController(from: splitController.viewControllers.last) + } else { + if let presentedController = controller?.presentedViewController { + return topController(from: presentedController) + } else { + if let child = controller?.children.last { + return topController(from: child) + } + return controller + } + } } } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 8e9667458..009cd19d6 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -378,7 +378,6 @@ public class Router: AuthorizationRouter, courseName: String, blockId: String, courseID: String, - sectionName: String, verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, @@ -398,7 +397,7 @@ public class Router: AuthorizationRouter, let config = Container.shared.resolve(ConfigProtocol.self) let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false - let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName, isDropdownActive: isDropdownActive) + let view = CourseUnitView(viewModel: viewModel, isDropdownActive: isDropdownActive) let controller = UIHostingController(rootView: view) navigationController.pushViewController(controller, animated: true) } @@ -417,7 +416,6 @@ public class Router: AuthorizationRouter, courseName: courseStructure.displayName, blockId: block.blockId, courseID: courseStructure.id, - sectionName: sequential.displayName, verticalIndex: verticalIndex, chapters: courseStructure.childs, chapterIndex: chapterIndex, @@ -444,7 +442,6 @@ public class Router: AuthorizationRouter, courseName: String, blockId: String, courseID: String, - sectionName: String, verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, @@ -480,13 +477,13 @@ public class Router: AuthorizationRouter, let config = Container.shared.resolve(ConfigProtocol.self) let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false + let isCourseNestedListEnabled = config?.uiComponents.courseNestedListEnabled ?? false - let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName, isDropdownActive: isDropdownActive) + let view = CourseUnitView(viewModel: viewModel, isDropdownActive: isDropdownActive) let controllerUnit = UIHostingController(rootView: view) var controllers = navigationController.viewControllers - if let config = container.resolve(ConfigProtocol.self), - config.uiComponents.courseNestedListEnabled { + if isCourseNestedListEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { controllers.removeLast(1) controllers.append(contentsOf: [controllerUnit]) } else { From 82c1648d6735dab71bd84c18d215fc33663114fd Mon Sep 17 00:00:00 2001 From: forgotvas Date: Thu, 28 Mar 2024 17:58:47 +0300 Subject: [PATCH 03/20] chore: removed useless comments --- OpenEdX/Managers/PipManager.swift | 83 ------------------------------- 1 file changed, 83 deletions(-) diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index d25095766..b06e3d7cd 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -175,89 +175,6 @@ public class PipManager: PipManagerProtocol { ) } } - /* - let isCourseOpened = hostCourseContainerView?.rootView.courseID == courseDetails.courseID - - if !isCourseOpened { - showTabScreen(tab: .dashboard) - - if courseDetails.isEnrolled { - showCourseScreens( - courseID: courseDetails.courseID, - isActive: nil, - courseStart: courseDetails.courseStart, - courseEnd: courseDetails.courseEnd, - enrollmentStart: courseDetails.enrollmentStart, - enrollmentEnd: courseDetails.enrollmentEnd, - title: courseDetails.courseTitle - ) - } else { - showCourseDetais( - courseID: courseDetails.courseID, - title: courseDetails.courseTitle - ) - } - } - - switch link.type { - case .courseVideos, - .courseDates, - .discussions, - .courseHandout, - .courseAnnouncement, - .courseDashboard: - popToCourseContainerView(animated: false) - default: - break - } - - DispatchQueue.main.asyncAfter(deadline: .now() + (isCourseOpened ? 0 : 1)) { - switch link.type { - case .courseDashboard: - self.hostCourseContainerView?.rootView.viewModel.selection = CourseTab.course.rawValue - case .courseVideos: - self.hostCourseContainerView?.rootView.viewModel.selection = CourseTab.videos.rawValue - case .courseDates: - self.hostCourseContainerView?.rootView.viewModel.selection = CourseTab.dates.rawValue - case .discussions, .discussionTopic, .discussionPost, .discussionComment: - self.hostCourseContainerView?.rootView.viewModel.selection = CourseTab.discussion.rawValue - case .courseHandout, .courseAnnouncement: - self.hostCourseContainerView?.rootView.viewModel.selection = CourseTab.handounds.rawValue - default: - break - } - - completion() - } - - public func showCourseScreens( - courseID: String, - isActive: Bool?, - courseStart: Date?, - courseEnd: Date?, - enrollmentStart: Date?, - enrollmentEnd: Date?, - title: String - ) { - let vm = Container.shared.resolve( - CourseContainerViewModel.self, - arguments: isActive, - courseStart, - courseEnd, - enrollmentStart, - enrollmentEnd - )! - let screensView = CourseContainerView( - viewModel: vm, - courseID: courseID, - title: title - ) - - let controller = UIHostingController(rootView: screensView) - navigationController.pushViewController(controller, animated: true) - } - - */ private var topCourseUnitController: UIHostingController? { router.getNavigationController().visibleViewController as? UIHostingController From c329f5396c93bc88a915295bd4ab94614a557c2a Mon Sep 17 00:00:00 2001 From: forgotvas Date: Thu, 28 Mar 2024 18:45:32 +0300 Subject: [PATCH 04/20] chore: refactor --- .../Unit/CourseUnitViewModel.swift | 7 +- OpenEdX/Managers/PipManager.swift | 47 ++++-------- OpenEdX/Router.swift | 75 ++++++++++++++----- 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index ca18d3250..8c05ae6cf 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -302,12 +302,7 @@ public class CourseUnitViewModel: ObservableObject { func route(to data: VerticalData?, animated: Bool = false) { guard let data = data else { return } - if data.verticalIndex == verticalIndex, - let block = blockFor(index: data.blockIndex, in: verticals[verticalIndex]) { - // if we are on same vertical now - lessonID = block.blockId - loadIndex() - } else if let vertical = vertical(for: data), + if let vertical = vertical(for: data), let block = blockFor(index: data.blockIndex, in: vertical) { router.replaceCourseUnit( courseName: courseName, diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index b06e3d7cd..17e572102 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -73,8 +73,7 @@ public class PipManager: PipManagerProtocol { return } } - - + // else create navigation stack and push new stack to root navigation controller try await navigate(to: holder) } @@ -114,22 +113,15 @@ public class PipManager: PipManagerProtocol { for (sequentialIndex, sequential) in chapter.childs.enumerated() { for (verticalIndex, vertical) in sequential.childs.enumerated() { for (_, block) in vertical.childs.enumerated() where block.id == holder.blockID { - let viewModel = Container.shared.resolve( - CourseUnitViewModel.self, - arguments: block.blockId, - courseStructure.id, - courseStructure.displayName, - courseStructure.childs, - chapterIndex, - sequentialIndex, - verticalIndex - )! - - let config = Container.shared.resolve(ConfigProtocol.self) - let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false - - let view = CourseUnitView(viewModel: viewModel, isDropdownActive: isDropdownActive) - return UIHostingController(rootView: view) + return router.getUnitController( + courseName: courseStructure.displayName, + blockId: block.blockId, + courseID: courseStructure.id, + verticalIndex: verticalIndex, + chapters: courseStructure.childs, + chapterIndex: chapterIndex, + sequentialIndex: verticalIndex + ) } } } @@ -144,22 +136,15 @@ public class PipManager: PipManagerProtocol { ) async throws -> UIHostingController { let courseDetails = try await getCourseDetails(for: holder) let isActive: Bool? = nil - - let vm = Container.shared.resolve( - CourseContainerViewModel.self, - arguments: isActive, - courseDetails.courseStart, - courseDetails.courseEnd, - courseDetails.enrollmentStart, - courseDetails.enrollmentEnd - )! - let screensView = CourseContainerView( - viewModel: vm, + let controller = getCourseScreensController( courseID: courseDetails.courseID, + isActive: isActive, + courseStart: courseDetails.courseStart, + courseEnd: courseDetails.courseEnd, + enrollmentStart: courseDetails.enrollmentStart, + enrollmentEnd: courseDetails.enrollmentEnd, title: courseDetails.courseTitle ) - - let controller = UIHostingController(rootView: screensView) controller.rootView.viewModel.selection = holder.selectedCourseTab return controller } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 009cd19d6..36306feb1 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -340,6 +340,27 @@ public class Router: AuthorizationRouter, enrollmentEnd: Date?, title: String ) { + let controller = getCourseScreensController( + courseID: courseID, + isActive: isActive, + courseStart: courseStart, + courseEnd: courseEnd, + enrollmentStart: enrollmentStart, + enrollmentEnd: enrollmentEnd, + title: title + ) + navigationController.pushViewController(controller, animated: true) + } + + public func getCourseScreensController( + courseID: String, + isActive: Bool?, + courseStart: Date?, + courseEnd: Date?, + enrollmentStart: Date?, + enrollmentEnd: Date?, + title: String + ) -> UIHostingController { let vm = Container.shared.resolve( CourseContainerViewModel.self, arguments: isActive, @@ -354,8 +375,7 @@ public class Router: AuthorizationRouter, title: title ) - let controller = UIHostingController(rootView: screensView) - navigationController.pushViewController(controller, animated: true) + return UIHostingController(rootView: screensView) } public func showHandoutsUpdatesView( @@ -383,6 +403,27 @@ public class Router: AuthorizationRouter, chapterIndex: Int, sequentialIndex: Int ) { + let controller = getUnitController( + courseName: courseName, + blockId: blockId, + courseID: courseID, + verticalIndex: verticalIndex, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + navigationController.pushViewController(controller, animated: true) + } + + public func getUnitController( + courseName: String, + blockId: String, + courseID: String, + verticalIndex: Int, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) -> UIHostingController { let viewModel = Container.shared.resolve( CourseUnitViewModel.self, arguments: blockId, @@ -398,8 +439,7 @@ public class Router: AuthorizationRouter, let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false let view = CourseUnitView(viewModel: viewModel, isDropdownActive: isDropdownActive) - let controller = UIHostingController(rootView: view) - navigationController.pushViewController(controller, animated: true) + return UIHostingController(rootView: view) } public func showCourseComponent( @@ -463,24 +503,21 @@ public class Router: AuthorizationRouter, viewModel: vmVertical ) let controllerVertical = UIHostingController(rootView: viewVertical) - - let viewModel = Container.shared.resolve( - CourseUnitViewModel.self, - arguments: blockId, - courseID, - courseName, - chapters, - chapterIndex, - sequentialIndex, - verticalIndex - )! + let controllerUnit = getUnitController( + courseName: courseName, + blockId: blockId, + courseID: courseID, + verticalIndex: verticalIndex, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + + let config = Container.shared.resolve(ConfigProtocol.self) - let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false let isCourseNestedListEnabled = config?.uiComponents.courseNestedListEnabled ?? false - - let view = CourseUnitView(viewModel: viewModel, isDropdownActive: isDropdownActive) - let controllerUnit = UIHostingController(rootView: view) + var controllers = navigationController.viewControllers if isCourseNestedListEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { From 42e539f7a64c7301f01f8ffb7ca3b1d2bef2d6ce Mon Sep 17 00:00:00 2001 From: forgotvas Date: Thu, 28 Mar 2024 18:46:35 +0300 Subject: [PATCH 05/20] chore: compilation error --- OpenEdX/Managers/PipManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index 17e572102..ea1d6539d 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -136,7 +136,7 @@ public class PipManager: PipManagerProtocol { ) async throws -> UIHostingController { let courseDetails = try await getCourseDetails(for: holder) let isActive: Bool? = nil - let controller = getCourseScreensController( + let controller = router.getCourseScreensController( courseID: courseDetails.courseID, isActive: isActive, courseStart: courseDetails.courseStart, From 5c8799fe5785fe25175dfd6651aec09244535ebe Mon Sep 17 00:00:00 2001 From: forgotvas Date: Thu, 28 Mar 2024 19:02:25 +0300 Subject: [PATCH 06/20] chore: fix navigation --- .../Unit/CourseUnitViewModel.swift | 20 +++++++++++-------- OpenEdX/Managers/PipManager.swift | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index 8c05ae6cf..b0667767b 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -61,7 +61,7 @@ public class CourseUnitViewModel: ObservableObject { case previous } - struct VerticalData { + struct VerticalData: Equatable { var chapterIndex: Int var sequentialIndex: Int var verticalIndex: Int @@ -220,12 +220,16 @@ public class CourseUnitViewModel: ObservableObject { // MARK: Navigation to next vertical var nextData: VerticalData? { nextData( - from: VerticalData( - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex, - verticalIndex: verticalIndex, - blockIndex: 0 - ) + from: currentData + ) + } + + var currentData: VerticalData { + VerticalData( + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex, + verticalIndex: verticalIndex, + blockIndex: index ) } @@ -301,7 +305,7 @@ public class CourseUnitViewModel: ObservableObject { } func route(to data: VerticalData?, animated: Bool = false) { - guard let data = data else { return } + guard let data = data, data != currentData else { return } if let vertical = vertical(for: data), let block = blockFor(index: data.blockIndex, in: vertical) { router.replaceCourseUnit( diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index ea1d6539d..c3cf6b55e 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -120,7 +120,7 @@ public class PipManager: PipManagerProtocol { verticalIndex: verticalIndex, chapters: courseStructure.childs, chapterIndex: chapterIndex, - sequentialIndex: verticalIndex + sequentialIndex: sequentialIndex ) } } From b0a1d0ec15398d2da5ec41b4d2f7f2902f506cd6 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Thu, 28 Mar 2024 19:07:23 +0300 Subject: [PATCH 07/20] chore: removed logs --- .../Video/EncodedVideoPlayerViewModel.swift | 2 -- .../Presentation/Video/PlayerViewController.swift | 2 -- .../Video/PlayerViewControllerHolder.swift | 14 +------------- OpenEdX/Managers/PipManager.swift | 1 - 4 files changed, 1 insertion(+), 18 deletions(-) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift index 5f4121c2c..2c7689a16 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift @@ -41,10 +41,8 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { courseID: courseID, selectedCourseTab: selectedCourseTab ) { - print("ALARM restore holder") controllerHolder = holder } else { - print("ALARM create holder") let holder = PlayerViewControllerHolder( url: url, blockID: blockID, diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift index 92e30b662..4671663fb 100644 --- a/Course/Course/Presentation/Video/PlayerViewController.swift +++ b/Course/Course/Presentation/Video/PlayerViewController.swift @@ -37,7 +37,6 @@ struct PlayerViewController: UIViewControllerRepresentable { return playerHolder.playerController } - print("ALARM create new player") let controller = playerHolder.playerController controller.modalPresentationStyle = .fullScreen controller.allowsPictureInPicturePlayback = true @@ -61,7 +60,6 @@ struct PlayerViewController: UIViewControllerRepresentable { func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) { let asset = playerController.player?.currentItem?.asset as? AVURLAsset if asset?.url.absoluteString != videoURL?.absoluteString && !playerHolder.isPipModeActive { - print("ALARM replace player") let player = context.coordinator.player(from: playerController) player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL!)) player?.currentItem?.preferredMaximumResolution = videoResolution diff --git a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift index c724995ca..2a1cde020 100644 --- a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift +++ b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift @@ -63,38 +63,26 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat Container.shared.resolve(PipManagerProtocol.self)?.set(holder: self) } -// func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) { -// -// } - public func playerViewController( _ playerViewController: AVPlayerViewController, failedToStartPictureInPictureWithError error: any Error ) { isPipModeActive = false Container.shared.resolve(PipManagerProtocol.self)?.remove(holder: self) - print("ALARM failed to start \(error)") } public func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { isPipModeActive = false Container.shared.resolve(PipManagerProtocol.self)?.remove(holder: self) - print("ALARM did stop picture in picture") } - -// func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { -// -// } + public func playerViewControllerRestoreUserInterfaceForPictureInPictureStop( _ playerViewController: AVPlayerViewController ) async -> Bool { - print("ALARM restore controller") do { try await Container.shared.resolve(PipManagerProtocol.self)?.restore(holder: self) - print("ALARM restore completed") return true } catch { - print("ALARM restore failed") return false } } diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index c3cf6b55e..db108c577 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -35,7 +35,6 @@ public class PipManager: PipManagerProtocol { courseID: String, selectedCourseTab: Int ) -> PlayerViewControllerHolder? { - print("ALARM navigationStack: \(router.getNavigationController().children)") if controllerHolder?.blockID == blockID, controllerHolder?.courseID == courseID, controllerHolder?.selectedCourseTab == selectedCourseTab { From 80d3e60b1992c2e4722f01228591fc969c937422 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Thu, 28 Mar 2024 19:18:46 +0300 Subject: [PATCH 08/20] chore: warnings --- OpenEdX/DI/ScreenAssembly.swift | 2 +- OpenEdX/Managers/PipManager.swift | 43 +++++-------------------------- OpenEdX/Router.swift | 3 +-- 3 files changed, 9 insertions(+), 39 deletions(-) diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 7a17c656d..db8829006 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -186,7 +186,7 @@ class ScreenAssembly: Assembly { } container.register(ProfileViewModel.self) { r in ProfileViewModel( - interactor: r.resolve(ProfileInteractorProtocol.self)!, + interactor: r.resolve(ProfileInteractorProtocol.self)!, downloadManager: r.resolve(DownloadManagerProtocol.self)!, router: r.resolve(ProfileRouter.self)!, analytics: r.resolve(ProfileAnalytics.self)!, diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index db108c577..c91f9b59d 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -5,13 +5,9 @@ // Created by Vadim Kuznetsov on 20.03.24. // -import Combine import Course import Discovery -import Foundation import SwiftUI -import Swinject -import Core public class PipManager: PipManagerProtocol { var controllerHolder: PlayerViewControllerHolder? @@ -77,7 +73,7 @@ public class PipManager: PipManagerProtocol { } @MainActor - func navigate(to holder: PlayerViewControllerHolder) async throws { + private func navigate(to holder: PlayerViewControllerHolder) async throws { let currentControllers = router.getNavigationController().viewControllers guard let mainController = currentControllers.first as? UIHostingController else { return @@ -99,8 +95,9 @@ public class PipManager: PipManagerProtocol { router.getNavigationController().setViewControllers(viewControllers, animated: true) } - - @MainActor func courseUnitController( + + @MainActor + private func courseUnitController( for holder: PlayerViewControllerHolder ) async throws -> UIHostingController { @@ -111,7 +108,7 @@ public class PipManager: PipManagerProtocol { for (chapterIndex, chapter) in courseStructure.childs.enumerated() { for (sequentialIndex, sequential) in chapter.childs.enumerated() { for (verticalIndex, vertical) in sequential.childs.enumerated() { - for (_, block) in vertical.childs.enumerated() where block.id == holder.blockID { + for block in vertical.childs where block.id == holder.blockID { return router.getUnitController( courseName: courseStructure.displayName, blockId: block.blockId, @@ -130,7 +127,7 @@ public class PipManager: PipManagerProtocol { } @MainActor - func containerController( + private func containerController( for holder: PlayerViewControllerHolder ) async throws -> UIHostingController { let courseDetails = try await getCourseDetails(for: holder) @@ -148,7 +145,7 @@ public class PipManager: PipManagerProtocol { return controller } - func getCourseDetails(for holder: PlayerViewControllerHolder) async throws -> CourseDetails { + private func getCourseDetails(for holder: PlayerViewControllerHolder) async throws -> CourseDetails { if let value = try? await discoveryInteractor.getLoadedCourseDetails( courseID: holder.courseID ) { @@ -167,32 +164,6 @@ public class PipManager: PipManagerProtocol { extension PipManager { enum PipManagerError: Error { - case cantGetCourseDetails case cantCreateCourseUnitView } } - -extension UIViewController { - public var mostTopController: UIViewController? { - topController(from: self) - } - - private func topController(from controller: UIViewController?) -> UIViewController? { - if let navigationController = controller as? UINavigationController { - return topController(from: navigationController.visibleViewController) - } else if let tabBarController = controller as? UITabBarController { - return topController(from: tabBarController.selectedViewController) - } else if let splitController = controller as? UISplitViewController { - return topController(from: splitController.viewControllers.last) - } else { - if let presentedController = controller?.presentedViewController { - return topController(from: presentedController) - } else { - if let child = controller?.children.last { - return topController(from: child) - } - return controller - } - } - } -} diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 36306feb1..a07db6df8 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -513,8 +513,7 @@ public class Router: AuthorizationRouter, chapterIndex: chapterIndex, sequentialIndex: sequentialIndex ) - - + let config = Container.shared.resolve(ConfigProtocol.self) let isCourseNestedListEnabled = config?.uiComponents.courseNestedListEnabled ?? false From 05c7fcde898c57f7256201a8c0bb9dd7e4115e43 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Thu, 28 Mar 2024 19:19:34 +0300 Subject: [PATCH 09/20] chore: refactor --- OpenEdX/Managers/PipManager.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index c91f9b59d..bc21855d1 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -11,7 +11,6 @@ import SwiftUI public class PipManager: PipManagerProtocol { var controllerHolder: PlayerViewControllerHolder? - private var restorationTask: Task? let discoveryInteractor: DiscoveryInteractorProtocol let courseInteractor: CourseInteractorProtocol let router: Router @@ -42,15 +41,11 @@ public class PipManager: PipManagerProtocol { public func set(holder: PlayerViewControllerHolder) { controllerHolder = holder - restorationTask?.cancel() - restorationTask = nil } public func remove(holder: PlayerViewControllerHolder) { if controllerHolder == holder { controllerHolder = nil - restorationTask?.cancel() - restorationTask = nil } } From 16ef27588225e2a4a96407de119be729a8f07d3d Mon Sep 17 00:00:00 2001 From: forgotvas Date: Thu, 28 Mar 2024 19:25:44 +0300 Subject: [PATCH 10/20] chore: refactor --- OpenEdX/Router.swift | 46 ++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index a07db6df8..d4a212ee0 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -314,6 +314,25 @@ public class Router: AuthorizationRouter, chapterIndex: Int, sequentialIndex: Int ) { + let controller = getVerticalController( + courseID: courseID, + courseName: courseName, + title: title, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + navigationController.pushViewController(controller, animated: true) + } + + public func getVerticalController( + courseID: String, + courseName: String, + title: String, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) -> UIHostingController { let viewModel = Container.shared.resolve( CourseVerticalViewModel.self, arguments: chapters, @@ -327,8 +346,7 @@ public class Router: AuthorizationRouter, courseID: courseID, viewModel: viewModel ) - let controller = UIHostingController(rootView: view) - navigationController.pushViewController(controller, animated: true) + return UIHostingController(rootView: view) } public func showCourseScreens( @@ -488,21 +506,6 @@ public class Router: AuthorizationRouter, sequentialIndex: Int, animated: Bool ) { - - let vmVertical = Container.shared.resolve( - CourseVerticalViewModel.self, - arguments: chapters, - chapterIndex, - sequentialIndex - )! - - let viewVertical = CourseVerticalView( - title: chapters[chapterIndex].childs[sequentialIndex].displayName, - courseName: courseName, - courseID: courseID, - viewModel: vmVertical - ) - let controllerVertical = UIHostingController(rootView: viewVertical) let controllerUnit = getUnitController( courseName: courseName, @@ -523,6 +526,15 @@ public class Router: AuthorizationRouter, controllers.removeLast(1) controllers.append(contentsOf: [controllerUnit]) } else { + let controllerVertical = getVerticalController( + courseID: courseID, + courseName: courseName, + title: chapters[chapterIndex].childs[sequentialIndex].displayName, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + controllers.removeLast(2) controllers.append(contentsOf: [controllerVertical, controllerUnit]) } From a9c856eebab5e3680dbf5ea7e6054254f650eef5 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Thu, 28 Mar 2024 19:48:06 +0300 Subject: [PATCH 11/20] chore: fallback for openEdX --- OpenEdX/DI/AppAssembly.swift | 3 ++- OpenEdX/Managers/PipManager.swift | 38 ++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 54a667cdb..295b9ae1c 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -202,7 +202,8 @@ class AppAssembly: Assembly { PipManager( router: r.resolve(Router.self)!, discoveryInteractor: r.resolve(DiscoveryInteractorProtocol.self)!, - courseInteractor: r.resolve(CourseInteractorProtocol.self)! + courseInteractor: r.resolve(CourseInteractorProtocol.self)!, + isNestedListEnabled: r.resolve(ConfigProtocol.self)?.uiComponents.courseNestedListEnabled ?? false ) }.inObjectScope(.container) } diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index bc21855d1..1c68b1ed6 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -14,14 +14,17 @@ public class PipManager: PipManagerProtocol { let discoveryInteractor: DiscoveryInteractorProtocol let courseInteractor: CourseInteractorProtocol let router: Router + let isNestedListEnabled: Bool public init( router: Router, discoveryInteractor: DiscoveryInteractorProtocol, - courseInteractor: CourseInteractorProtocol + courseInteractor: CourseInteractorProtocol, + isNestedListEnabled: Bool ) { self.discoveryInteractor = discoveryInteractor self.courseInteractor = courseInteractor self.router = router + self.isNestedListEnabled = isNestedListEnabled } public func holder( @@ -86,11 +89,43 @@ public class PipManager: PipManagerProtocol { viewControllers.append(try await containerController(for: holder)) } + if !isNestedListEnabled && holder.selectedCourseTab != CourseTab.dates.rawValue { + viewControllers.append(try await courseVerticalController(for: holder)) + } + viewControllers.append(try await courseUnitController(for: holder)) router.getNavigationController().setViewControllers(viewControllers, animated: true) } + @MainActor + private func courseVerticalController( + for holder: PlayerViewControllerHolder + ) async throws -> UIHostingController { + var courseStructure = try await courseInteractor.getLoadedCourseBlocks(courseID: holder.courseID) + if holder.selectedCourseTab == CourseTab.videos.rawValue { + courseStructure = courseInteractor.getCourseVideoBlocks(fullStructure: courseStructure) + } + for (chapterIndex, chapter) in courseStructure.childs.enumerated() { + for (sequentialIndex, sequential) in chapter.childs.enumerated() { + for (verticalIndex, vertical) in sequential.childs.enumerated() { + for block in vertical.childs where block.id == holder.blockID { + return router.getVerticalController( + courseID: holder.courseID, + courseName: courseStructure.displayName, + title: courseStructure.childs[chapterIndex].childs[sequentialIndex].displayName, + chapters: courseStructure.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + } + } + } + } + + throw PipManagerError.cantCreateCourseVerticalView + } + @MainActor private func courseUnitController( for holder: PlayerViewControllerHolder @@ -160,5 +195,6 @@ public class PipManager: PipManagerProtocol { extension PipManager { enum PipManagerError: Error { case cantCreateCourseUnitView + case cantCreateCourseVerticalView } } From 7a6637859c3a53672603ddbd5266b414447c9ed7 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Fri, 29 Mar 2024 18:28:00 +0300 Subject: [PATCH 12/20] fix: Video starts playing on neighboring cards... without video #362 --- Course/Course/Presentation/Unit/CourseUnitView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 1219cea91..b6be80a9f 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -171,7 +171,7 @@ public struct CourseUnitView: View { switch LessonType.from(block, streamingQuality: viewModel.streamingQuality) { // MARK: YouTube case let .youtube(url, blockID): - if index >= viewModel.index - 1 && index <= viewModel.index + 1 { + if index == viewModel.index { if viewModel.connectivity.isInternetAvaliable { YouTubeView( name: block.displayName, From 575eabcdae34e7938c833bddf830692df83911bc Mon Sep 17 00:00:00 2001 From: forgotvas Date: Fri, 29 Mar 2024 18:47:38 +0300 Subject: [PATCH 13/20] fix: do not play video when jump to new vertical --- Course/Course/Presentation/Unit/CourseUnitView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index b6be80a9f..67d89ffaa 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -121,6 +121,7 @@ public struct CourseUnitView: View { ) { [weak viewModel] vertical in let data = viewModel?.dataFor(blockId: vertical.childs.first?.id) viewModel?.route(to: data) + playerStateSubject.send(VideoPlayerState.kill) } } } From 433e8458538d16d6ab5cc0dffb4a3081fff22dd2 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Fri, 29 Mar 2024 19:27:29 +0300 Subject: [PATCH 14/20] chore: pause pip when player begins to play --- .../Video/EncodedVideoPlayer.swift | 5 ++- .../Video/EncodedVideoPlayerViewModel.swift | 4 +- .../Video/PlayerViewController.swift | 10 +++-- .../Video/PlayerViewControllerHolder.swift | 40 +++++++++++++++---- OpenEdX/Managers/PipManager.swift | 9 +++++ 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index c3f64dc17..ac6411299 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -81,7 +81,10 @@ public struct EncodedVideoPlayer: View { .frame(minWidth: isHorizontal ? reader.size.width * 0.6 : 380) .cornerRadius(12) .onAppear { - viewModel.controller.player?.play() + if !viewModel.controllerHolder.isPlayingInPip, + !viewModel.controllerHolder.isOtherPlayerInPip { + viewModel.controller.player?.play() + } } if isHorizontal { Spacer() diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift index 2c7689a16..8a3a48a84 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift @@ -63,11 +63,11 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { playerStateSubject.sink(receiveValue: { [weak self] state in switch state { case .pause: - if self?.controllerHolder.isPipModeActive != true { + if self?.controllerHolder.isPlayingInPip != true { self?.controller.player?.pause() } case .kill: - if self?.controllerHolder.isPipModeActive != true { + if self?.controllerHolder.isPlayingInPip != true { self?.controller.player?.replaceCurrentItem(with: nil) } case .none: diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift index 4671663fb..ed22537c8 100644 --- a/Course/Course/Presentation/Video/PlayerViewController.swift +++ b/Course/Course/Presentation/Video/PlayerViewController.swift @@ -33,7 +33,8 @@ struct PlayerViewController: UIViewControllerRepresentable { } func makeUIViewController(context: Context) -> AVPlayerViewController { - if playerHolder.isPipModeActive { + context.coordinator.currentHolder = playerHolder + if playerHolder.isPlayingInPip { return playerHolder.playerController } @@ -59,7 +60,7 @@ struct PlayerViewController: UIViewControllerRepresentable { func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) { let asset = playerController.player?.currentItem?.asset as? AVURLAsset - if asset?.url.absoluteString != videoURL?.absoluteString && !playerHolder.isPipModeActive { + if asset?.url.absoluteString != videoURL?.absoluteString && !playerHolder.isPlayingInPip { let player = context.coordinator.player(from: playerController) player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL!)) player?.currentItem?.preferredMaximumResolution = videoResolution @@ -83,6 +84,7 @@ struct PlayerViewController: UIViewControllerRepresentable { var currentPlayer: AVPlayer? var observer: Any? var cancellations: [AnyCancellable] = [] + weak var currentHolder: PlayerViewControllerHolder? func player(from playerController: AVPlayerViewController) -> AVPlayer? { var player = playerController.player @@ -117,9 +119,9 @@ struct PlayerViewController: UIViewControllerRepresentable { } player.publisher(for: \.rate) - .sink { rate in + .sink {[weak self] rate in guard rate > 0 else { return } - + self?.currentHolder?.pausePipIfNeed() } .store(in: &cancellations) diff --git a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift index 2a1cde020..badd5387c 100644 --- a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift +++ b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift @@ -10,14 +10,21 @@ import Combine import Swinject public protocol PipManagerProtocol { + var isPipActive: Bool { get } + func holder(for url: URL?, blockID: String, courseID: String, selectedCourseTab: Int) -> PlayerViewControllerHolder? func set(holder: PlayerViewControllerHolder) func remove(holder: PlayerViewControllerHolder) func restore(holder: PlayerViewControllerHolder) async throws + func pauseCurrentPipVideo() } #if DEBUG public class PipManagerProtocolMock: PipManagerProtocol { + public var isPipActive: Bool { + false + } + public init() {} public func holder( for url: URL?, @@ -30,6 +37,7 @@ public class PipManagerProtocolMock: PipManagerProtocol { public func set(holder: PlayerViewControllerHolder) {} public func remove(holder: PlayerViewControllerHolder) {} public func restore(holder: PlayerViewControllerHolder) async throws {} + public func pauseCurrentPipVideo() {} } #endif @@ -38,7 +46,18 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat public let blockID: String public let courseID: String public let selectedCourseTab: Int - public var isPipModeActive: Bool = false + public var isPlayingInPip: Bool = false + public var isOtherPlayerInPip: Bool { + let holder = pipManager.holder( + for: url, + blockID: blockID, + courseID: courseID, + selectedCourseTab: selectedCourseTab + ) + return holder == nil && pipManager.isPipActive + } + + private let pipManager: PipManagerProtocol public lazy var playerController: AVPlayerViewController = { let playerController = AVPlayerViewController() @@ -56,24 +75,25 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat self.blockID = blockID self.courseID = courseID self.selectedCourseTab = selectedCourseTab + self.pipManager = Container.shared.resolve(PipManagerProtocol.self)! } public func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { - isPipModeActive = true - Container.shared.resolve(PipManagerProtocol.self)?.set(holder: self) + isPlayingInPip = true + pipManager.set(holder: self) } public func playerViewController( _ playerViewController: AVPlayerViewController, failedToStartPictureInPictureWithError error: any Error ) { - isPipModeActive = false - Container.shared.resolve(PipManagerProtocol.self)?.remove(holder: self) + isPlayingInPip = false + pipManager.remove(holder: self) } public func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { - isPipModeActive = false - Container.shared.resolve(PipManagerProtocol.self)?.remove(holder: self) + isPlayingInPip = false + pipManager.remove(holder: self) } public func playerViewControllerRestoreUserInterfaceForPictureInPictureStop( @@ -96,4 +116,10 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat blockID == object.blockID && selectedCourseTab == object.selectedCourseTab } + + public func pausePipIfNeed() { + if !isPlayingInPip { + pipManager.pauseCurrentPipVideo() + } + } } diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index 1c68b1ed6..21cde8a19 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -15,6 +15,10 @@ public class PipManager: PipManagerProtocol { let courseInteractor: CourseInteractorProtocol let router: Router let isNestedListEnabled: Bool + public var isPipActive: Bool { + controllerHolder != nil + } + public init( router: Router, discoveryInteractor: DiscoveryInteractorProtocol, @@ -70,6 +74,11 @@ public class PipManager: PipManagerProtocol { try await navigate(to: holder) } + public func pauseCurrentPipVideo() { + guard let holder = controllerHolder else { return } + holder.playerController.player?.pause() + } + @MainActor private func navigate(to holder: PlayerViewControllerHolder) async throws { let currentControllers = router.getNavigationController().viewControllers From 8f9756f5c6c228402e2cbd81181759ae38aa902b Mon Sep 17 00:00:00 2001 From: forgotvas Date: Fri, 29 Mar 2024 19:41:05 +0300 Subject: [PATCH 15/20] chore: pause pip when youtube starts --- .../Presentation/Video/PlayerViewControllerHolder.swift | 2 +- .../Course/Presentation/Video/YouTubeVideoPlayer.swift | 9 ++++++--- .../Presentation/Video/YouTubeVideoPlayerViewModel.swift | 9 ++++++--- OpenEdX/DI/ScreenAssembly.swift | 3 ++- OpenEdX/Managers/PipManager.swift | 1 + 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift index badd5387c..a1f21aa79 100644 --- a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift +++ b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift @@ -53,7 +53,7 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat blockID: blockID, courseID: courseID, selectedCourseTab: selectedCourseTab - ) + ) return holder == nil && pipManager.isPipActive } diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index 08a868665..82955ee63 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -88,10 +88,13 @@ struct YouTubeVideoPlayer_Previews: PreviewProvider { languages: [], playerStateSubject: CurrentValueSubject(nil), interactor: CourseInteractor(repository: CourseRepositoryMock()), - router: CourseRouterMock(), + router: CourseRouterMock(), appStorage: CoreStorageMock(), - connectivity: Connectivity()), - isOnScreen: true) + connectivity: Connectivity(), + pipManager: PipManagerProtocolMock() + ), + isOnScreen: true + ) } } #endif diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift index e3486d600..02524b33b 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift @@ -23,6 +23,7 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { private var duration: Double? private var isViewedOnce: Bool = false private var url: String + private let pipManager: PipManagerProtocol public init( url: String, @@ -33,13 +34,14 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { interactor: CourseInteractorProtocol, router: CourseRouter, appStorage: CoreStorage, - connectivity: ConnectivityProtocol + connectivity: ConnectivityProtocol, + pipManager: PipManagerProtocol ) { self.url = url let videoID = url.replacingOccurrences(of: "https://www.youtube.com/watch?v=", with: "") let configuration = YouTubePlayer.Configuration(configure: { - $0.autoPlay = true + $0.autoPlay = !pipManager.isPipActive $0.playInline = true $0.showFullscreenButton = true $0.allowsPictureInPictureMediaPlayback = false @@ -55,7 +57,7 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { """ }) self.youtubePlayer = YouTubePlayer(source: .video(id: videoID), configuration: configuration) - + self.pipManager = pipManager super.init( blockID: blockID, courseID: courseID, @@ -123,6 +125,7 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { self.play = false case .playing: self.play = true + self.pipManager.pauseCurrentPipVideo() case .paused: self.play = false case .buffering, .cued: diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index db8829006..4ca9a3867 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -320,7 +320,8 @@ class ScreenAssembly: Assembly { interactor: r.resolve(CourseInteractorProtocol.self)!, router: r.resolve(CourseRouter.self)!, appStorage: r.resolve(CoreStorage.self)!, - connectivity: r.resolve(ConnectivityProtocol.self)! + connectivity: r.resolve(ConnectivityProtocol.self)!, + pipManager: r.resolve(PipManagerProtocol.self)! ) } diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index 21cde8a19..c2446f474 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -48,6 +48,7 @@ public class PipManager: PipManagerProtocol { public func set(holder: PlayerViewControllerHolder) { controllerHolder = holder + print("ALARM \(holder.playerController.player)") } public func remove(holder: PlayerViewControllerHolder) { From b0d6edea92d9a3ee19007cfee05f50b739e1e089 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Fri, 29 Mar 2024 20:01:51 +0300 Subject: [PATCH 16/20] chore: pause players when pip plays --- .../Video/PlayerViewController.swift | 10 +++++++++- .../Video/PlayerViewControllerHolder.swift | 18 ++++++++++++------ .../Video/YouTubeVideoPlayerViewModel.swift | 7 +++++++ OpenEdX/Managers/PipManager.swift | 19 ++++++++++++++++++- 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift index ed22537c8..5ac17bcc5 100644 --- a/Course/Course/Presentation/Video/PlayerViewController.swift +++ b/Course/Course/Presentation/Video/PlayerViewController.swift @@ -124,7 +124,15 @@ struct PlayerViewController: UIViewControllerRepresentable { self?.currentHolder?.pausePipIfNeed() } .store(in: &cancellations) - + currentHolder?.pipRatePublisher()? + .sink {[weak self] rate in + guard rate > 0 else { return } + if self?.currentHolder?.isPlayingInPip == false { + self?.currentPlayer?.pause() + } + } + .store(in: &cancellations) + currentPlayer = player } diff --git a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift index a1f21aa79..3ba64b192 100644 --- a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift +++ b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift @@ -16,6 +16,7 @@ public protocol PipManagerProtocol { func set(holder: PlayerViewControllerHolder) func remove(holder: PlayerViewControllerHolder) func restore(holder: PlayerViewControllerHolder) async throws + func pipRatePublisher() -> AnyPublisher? func pauseCurrentPipVideo() } @@ -37,6 +38,7 @@ public class PipManagerProtocolMock: PipManagerProtocol { public func set(holder: PlayerViewControllerHolder) {} public func remove(holder: PlayerViewControllerHolder) {} public func restore(holder: PlayerViewControllerHolder) async throws {} + public func pipRatePublisher() -> AnyPublisher? { nil } public func pauseCurrentPipVideo() {} } #endif @@ -56,15 +58,15 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat ) return holder == nil && pipManager.isPipActive } - + private let pipManager: PipManagerProtocol - + public lazy var playerController: AVPlayerViewController = { let playerController = AVPlayerViewController() playerController.delegate = self return playerController }() - + public init( url: URL?, blockID: String, @@ -77,12 +79,12 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat self.selectedCourseTab = selectedCourseTab self.pipManager = Container.shared.resolve(PipManagerProtocol.self)! } - + public func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { isPlayingInPip = true pipManager.set(holder: self) } - + public func playerViewController( _ playerViewController: AVPlayerViewController, failedToStartPictureInPictureWithError error: any Error @@ -95,7 +97,7 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat isPlayingInPip = false pipManager.remove(holder: self) } - + public func playerViewControllerRestoreUserInterfaceForPictureInPictureStop( _ playerViewController: AVPlayerViewController ) async -> Bool { @@ -122,4 +124,8 @@ public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegat pipManager.pauseCurrentPipVideo() } } + + public func pipRatePublisher() -> AnyPublisher? { + pipManager.pipRatePublisher() + } } diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift index 02524b33b..92bdad530 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift @@ -139,5 +139,12 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { self.isLoading = false } }).store(in: &subscription) + + pipManager.pipRatePublisher()? + .sink {[weak self] rate in + guard rate > 0 else { return } + self?.youtubePlayer.pause() + } + .store(in: &subscription) } } diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index c2446f474..63a6d4498 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -6,6 +6,7 @@ // import Course +import Combine import Discovery import SwiftUI @@ -19,6 +20,9 @@ public class PipManager: PipManagerProtocol { controllerHolder != nil } + private var ratePublisher: PassthroughSubject? + private var cancellations: [AnyCancellable] = [] + public init( router: Router, discoveryInteractor: DiscoveryInteractorProtocol, @@ -48,15 +52,28 @@ public class PipManager: PipManagerProtocol { public func set(holder: PlayerViewControllerHolder) { controllerHolder = holder - print("ALARM \(holder.playerController.player)") + ratePublisher = PassthroughSubject() + cancellations.removeAll() + holder.playerController.player?.publisher(for: \.rate) + .sink { [weak self] rate in + self?.ratePublisher?.send(rate) + } + .store(in: &cancellations) } public func remove(holder: PlayerViewControllerHolder) { if controllerHolder == holder { controllerHolder = nil + cancellations.removeAll() + ratePublisher = nil } } + public func pipRatePublisher() -> AnyPublisher? { + ratePublisher? + .eraseToAnyPublisher() + } + @MainActor public func restore(holder: PlayerViewControllerHolder) async throws { let courseID = holder.courseID From c5478435556e0b591c7111f936b49e651878bd46 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Fri, 29 Mar 2024 20:11:59 +0300 Subject: [PATCH 17/20] chore: merge conflict --- OpenEdX/Router.swift | 122 +++++++++++++++++++++++++++++-------------- 1 file changed, 84 insertions(+), 38 deletions(-) diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index c517a3cc6..9f8f0fbd6 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -320,6 +320,25 @@ public class Router: AuthorizationRouter, chapterIndex: Int, sequentialIndex: Int ) { + let controller = getVerticalController( + courseID: courseID, + courseName: courseName, + title: title, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + navigationController.pushViewController(controller, animated: true) + } + + public func getVerticalController( + courseID: String, + courseName: String, + title: String, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) -> UIHostingController { let viewModel = Container.shared.resolve( CourseVerticalViewModel.self, arguments: chapters, @@ -333,8 +352,7 @@ public class Router: AuthorizationRouter, courseID: courseID, viewModel: viewModel ) - let controller = UIHostingController(rootView: view) - navigationController.pushViewController(controller, animated: true) + return UIHostingController(rootView: view) } public func showCourseScreens( @@ -346,6 +364,27 @@ public class Router: AuthorizationRouter, enrollmentEnd: Date?, title: String ) { + let controller = getCourseScreensController( + courseID: courseID, + isActive: isActive, + courseStart: courseStart, + courseEnd: courseEnd, + enrollmentStart: enrollmentStart, + enrollmentEnd: enrollmentEnd, + title: title + ) + navigationController.pushViewController(controller, animated: true) + } + + public func getCourseScreensController( + courseID: String, + isActive: Bool?, + courseStart: Date?, + courseEnd: Date?, + enrollmentStart: Date?, + enrollmentEnd: Date?, + title: String + ) -> UIHostingController { let vm = Container.shared.resolve( CourseContainerViewModel.self, arguments: isActive, @@ -360,8 +399,7 @@ public class Router: AuthorizationRouter, title: title ) - let controller = UIHostingController(rootView: screensView) - navigationController.pushViewController(controller, animated: true) + return UIHostingController(rootView: screensView) } public func showHandoutsUpdatesView( @@ -384,12 +422,32 @@ public class Router: AuthorizationRouter, courseName: String, blockId: String, courseID: String, - sectionName: String, verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, sequentialIndex: Int ) { + let controller = getUnitController( + courseName: courseName, + blockId: blockId, + courseID: courseID, + verticalIndex: verticalIndex, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + navigationController.pushViewController(controller, animated: true) + } + + public func getUnitController( + courseName: String, + blockId: String, + courseID: String, + verticalIndex: Int, + chapters: [CourseChapter], + chapterIndex: Int, + sequentialIndex: Int + ) -> UIHostingController { let viewModel = Container.shared.resolve( CourseUnitViewModel.self, arguments: blockId, @@ -404,9 +462,8 @@ public class Router: AuthorizationRouter, let config = Container.shared.resolve(ConfigProtocol.self) let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false - let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName, isDropdownActive: isDropdownActive) - let controller = UIHostingController(rootView: view) - navigationController.pushViewController(controller, animated: true) + let view = CourseUnitView(viewModel: viewModel, isDropdownActive: isDropdownActive) + return UIHostingController(rootView: view) } public func showCourseComponent( @@ -488,52 +545,41 @@ public class Router: AuthorizationRouter, courseName: String, blockId: String, courseID: String, - sectionName: String, verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, sequentialIndex: Int, animated: Bool ) { - - let vmVertical = Container.shared.resolve( - CourseVerticalViewModel.self, - arguments: chapters, - chapterIndex, - sequentialIndex - )! - - let viewVertical = CourseVerticalView( - title: chapters[chapterIndex].childs[sequentialIndex].displayName, + + let controllerUnit = getUnitController( courseName: courseName, + blockId: blockId, courseID: courseID, - viewModel: vmVertical + verticalIndex: verticalIndex, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex ) - let controllerVertical = UIHostingController(rootView: viewVertical) - - let viewModel = Container.shared.resolve( - CourseUnitViewModel.self, - arguments: blockId, - courseID, - courseName, - chapters, - chapterIndex, - sequentialIndex, - verticalIndex - )! let config = Container.shared.resolve(ConfigProtocol.self) - let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false - - let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName, isDropdownActive: isDropdownActive) - let controllerUnit = UIHostingController(rootView: view) + let isCourseNestedListEnabled = config?.uiComponents.courseNestedListEnabled ?? false + var controllers = navigationController.viewControllers - if let config = container.resolve(ConfigProtocol.self), - config.uiComponents.courseNestedListEnabled { + if isCourseNestedListEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { controllers.removeLast(1) controllers.append(contentsOf: [controllerUnit]) } else { + let controllerVertical = getVerticalController( + courseID: courseID, + courseName: courseName, + title: chapters[chapterIndex].childs[sequentialIndex].displayName, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + controllers.removeLast(2) controllers.append(contentsOf: [controllerVertical, controllerUnit]) } From c68550dc196aa54a9a75f703de2b35064985ec0c Mon Sep 17 00:00:00 2001 From: forgotvas Date: Fri, 29 Mar 2024 20:14:23 +0300 Subject: [PATCH 18/20] chore: merge conflict --- OpenEdX/Router.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 9f8f0fbd6..4ed940a18 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -500,7 +500,6 @@ public class Router: AuthorizationRouter, courseName: courseStructure.displayName, blockId: block.blockId, courseID: courseStructure.id, - sectionName: courseName ?? "", verticalIndex: verticalPosition ?? 0, chapters: courseStructure.childs, chapterIndex: chapterPosition ?? 0, From 8a6da8af5bd39be8885c2af4bfbe872cd366b046 Mon Sep 17 00:00:00 2001 From: forgotvas Date: Fri, 29 Mar 2024 23:27:52 +0300 Subject: [PATCH 19/20] chore: removed useless code --- .../Course/Presentation/Video/EncodedVideoPlayerViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift index 8a3a48a84..bb5eb8d3e 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift @@ -8,7 +8,6 @@ import _AVKit_SwiftUI import Core import Combine -import Swinject public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { From 14be4c35ce2f4298eaa2d6920bf6f079913172bb Mon Sep 17 00:00:00 2001 From: forgotvas Date: Sat, 30 Mar 2024 00:19:01 +0300 Subject: [PATCH 20/20] chore: little refactor --- .../Presentation/Unit/CourseUnitView.swift | 2 +- .../Unit/CourseUnitViewModel.swift | 63 ++++++++++--------- .../Video/PlayerViewController.swift | 9 +-- OpenEdX/DI/AppAssembly.swift | 4 -- OpenEdX/Managers/PipManager.swift | 55 +++++++--------- 5 files changed, 65 insertions(+), 68 deletions(-) diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 2b00da51b..dff0a0ea9 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -119,7 +119,7 @@ public struct CourseUnitView: View { offsetY: isHorizontal ? landscapeTopSpacing : portraitTopSpacing, showDropdown: $showDropdown ) { [weak viewModel] vertical in - let data = viewModel?.dataFor(blockId: vertical.childs.first?.id) + let data = VerticalData.dataFor(blockId: vertical.childs.first?.id, in: viewModel?.chapters ?? []) viewModel?.route(to: data) playerStateSubject.send(VideoPlayerState.kill) } diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index 221c8942e..8f4be45b8 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -54,6 +54,39 @@ public enum LessonType: Equatable { } } +public struct VerticalData: Equatable { + public var chapterIndex: Int + public var sequentialIndex: Int + public var verticalIndex: Int + public var blockIndex: Int + + public init(chapterIndex: Int, sequentialIndex: Int, verticalIndex: Int, blockIndex: Int) { + self.chapterIndex = chapterIndex + self.sequentialIndex = sequentialIndex + self.verticalIndex = verticalIndex + self.blockIndex = blockIndex + } + + public static func dataFor(blockId: String?, in chapters: [CourseChapter]) -> VerticalData? { + guard let blockId = blockId else { return nil } + for (chapterIndex, chapter) in chapters.enumerated() { + for (sequentialIndex, sequential) in chapter.childs.enumerated() { + for (verticalIndex, vertical) in sequential.childs.enumerated() { + for (blockIndex, block) in vertical.childs.enumerated() where block.id.contains(blockId) { + return VerticalData( + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex, + verticalIndex: verticalIndex, + blockIndex: blockIndex + ) + } + } + } + } + return nil + } +} + public class CourseUnitViewModel: ObservableObject { enum LessonAction { @@ -61,13 +94,6 @@ public class CourseUnitViewModel: ObservableObject { case previous } - struct VerticalData: Equatable { - var chapterIndex: Int - var sequentialIndex: Int - var verticalIndex: Int - var blockIndex: Int - } - var verticals: [CourseVertical] var verticalIndex: Int var courseName: String @@ -310,7 +336,7 @@ public class CourseUnitViewModel: ObservableObject { let block = blockFor(index: data.blockIndex, in: vertical) { router.replaceCourseUnit( courseName: courseName, - blockId: block.id, + blockId: block.blockId, courseID: courseID, verticalIndex: data.verticalIndex, chapters: chapters, @@ -322,29 +348,10 @@ public class CourseUnitViewModel: ObservableObject { } public func route(to blockId: String?) { - guard let data = dataFor(blockId: blockId) else { return } + guard let data = VerticalData.dataFor(blockId: blockId, in: chapters) else { return } route(to: data, animated: true) } - func dataFor(blockId: String?) -> VerticalData? { - guard let blockId = blockId else { return nil } - for (chapterIndex, chapter) in chapters.enumerated() { - for (sequentialIndex, sequential) in chapter.childs.enumerated() { - for (verticalIndex, vertical) in sequential.childs.enumerated() { - for (blockIndex, block) in vertical.childs.enumerated() where block.id.contains(blockId) { - return VerticalData( - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex, - verticalIndex: verticalIndex, - blockIndex: blockIndex - ) - } - } - } - } - return nil - } - public var currentCourseId: String { courseID } diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift index 5ac17bcc5..0bb477635 100644 --- a/Course/Course/Presentation/Video/PlayerViewController.swift +++ b/Course/Course/Presentation/Video/PlayerViewController.swift @@ -97,11 +97,12 @@ struct PlayerViewController: UIViewControllerRepresentable { } func setPlayer(_ player: AVPlayer?, currentProgress: @escaping ((Float, Double) -> Void)) { - guard let player = player else { return } cancellations.removeAll() if let observer = observer { currentPlayer?.removeTimeObserver(observer) - currentPlayer?.pause() + if currentHolder?.isPlayingInPip == false { + currentPlayer?.pause() + } } let interval = CMTime( @@ -109,7 +110,7 @@ struct PlayerViewController: UIViewControllerRepresentable { preferredTimescale: CMTimeScale(NSEC_PER_SEC) ) - observer = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) {[weak player] time in + observer = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) {[weak player] time in var progress: Float = .zero let currentSeconds = CMTimeGetSeconds(time) guard let duration = player?.currentItem?.duration else { return } @@ -118,7 +119,7 @@ struct PlayerViewController: UIViewControllerRepresentable { currentProgress(progress, currentSeconds) } - player.publisher(for: \.rate) + player?.publisher(for: \.rate) .sink {[weak self] rate in guard rate > 0 else { return } self?.currentHolder?.pausePipIfNeed() diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 4998e62f1..12ee2d514 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -127,10 +127,6 @@ class AppAssembly: Assembly { r.resolve(Router.self)! }.inObjectScope(.container) - container.register(DeepLinkRouter.self) { r in - r.resolve(Router.self)! - }.inObjectScope(.container) - container.register(ConfigProtocol.self) { _ in Config() }.inObjectScope(.container) diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index 63a6d4498..8720ae03f 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -133,21 +133,16 @@ public class PipManager: PipManagerProtocol { if holder.selectedCourseTab == CourseTab.videos.rawValue { courseStructure = courseInteractor.getCourseVideoBlocks(fullStructure: courseStructure) } - for (chapterIndex, chapter) in courseStructure.childs.enumerated() { - for (sequentialIndex, sequential) in chapter.childs.enumerated() { - for (verticalIndex, vertical) in sequential.childs.enumerated() { - for block in vertical.childs where block.id == holder.blockID { - return router.getVerticalController( - courseID: holder.courseID, - courseName: courseStructure.displayName, - title: courseStructure.childs[chapterIndex].childs[sequentialIndex].displayName, - chapters: courseStructure.childs, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) - } - } - } + + if let data = VerticalData.dataFor(blockId: holder.blockID, in: courseStructure.childs) { + return router.getVerticalController( + courseID: holder.courseID, + courseName: courseStructure.displayName, + title: courseStructure.childs[data.chapterIndex].childs[data.sequentialIndex].displayName, + chapters: courseStructure.childs, + chapterIndex: data.chapterIndex, + sequentialIndex: data.sequentialIndex + ) } throw PipManagerError.cantCreateCourseVerticalView @@ -162,22 +157,20 @@ public class PipManager: PipManagerProtocol { if holder.selectedCourseTab == CourseTab.videos.rawValue { courseStructure = courseInteractor.getCourseVideoBlocks(fullStructure: courseStructure) } - for (chapterIndex, chapter) in courseStructure.childs.enumerated() { - for (sequentialIndex, sequential) in chapter.childs.enumerated() { - for (verticalIndex, vertical) in sequential.childs.enumerated() { - for block in vertical.childs where block.id == holder.blockID { - return router.getUnitController( - courseName: courseStructure.displayName, - blockId: block.blockId, - courseID: courseStructure.id, - verticalIndex: verticalIndex, - chapters: courseStructure.childs, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) - } - } - } + if let data = VerticalData.dataFor(blockId: holder.blockID, in: courseStructure.childs) { + let chapter = courseStructure.childs[data.chapterIndex] + let sequential = chapter.childs[data.sequentialIndex] + let vertical = sequential.childs[data.verticalIndex] + let block = vertical.childs[data.blockIndex] + return router.getUnitController( + courseName: courseStructure.displayName, + blockId: block.id, + courseID: courseStructure.id, + verticalIndex: data.verticalIndex, + chapters: courseStructure.childs, + chapterIndex: data.chapterIndex, + sequentialIndex: data.sequentialIndex + ) } throw PipManagerError.cantCreateCourseUnitView