diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index fb7deb25b..6d7eab45a 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 */; }; @@ -150,6 +151,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 = ""; }; @@ -445,6 +447,7 @@ 070019AA28F6F79E00D5FC78 /* Video */ = { isa = PBXGroup; children = ( + 065275342BB1B39C0093BCCA /* PlayerViewControllerHolder.swift */, 02F066E729DC71750073E13B /* SubtittlesView.swift */, 0766DFCB299AA7A600EBEF6A /* YouTubeVideoPlayer.swift */, 022F8E152A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift */, @@ -830,6 +833,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/CourseRouter.swift b/Course/Course/Presentation/CourseRouter.swift index 607c51ac5..570b5a09c 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, @@ -75,7 +73,6 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { courseName: String, blockId: String, courseID: String, - sectionName: String, verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, @@ -86,7 +83,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 cf2ec1854..54a069049 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -106,7 +106,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 fed1ba259..6e8ad3927 100644 --- a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift +++ b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift @@ -226,7 +226,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 c33025a8f..d16095762 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 fbcc8ef84..334b3cf08 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 } @@ -128,7 +125,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, @@ -170,7 +166,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 56c387965..dff0a0ea9 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,9 @@ public struct CourseUnitView: View { offsetY: isHorizontal ? landscapeTopSpacing : portraitTopSpacing, showDropdown: $showDropdown ) { [weak viewModel] vertical in - viewModel?.route(to: vertical) + let data = VerticalData.dataFor(blockId: vertical.childs.first?.id, in: viewModel?.chapters ?? []) + viewModel?.route(to: data) + playerStateSubject.send(VideoPlayerState.kill) } } } @@ -173,7 +172,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, @@ -418,7 +417,6 @@ public struct CourseUnitView: View { Spacer() } CourseNavigationView( - sectionName: sectionName, viewModel: viewModel, playerStateSubject: playerStateSubject ) @@ -566,7 +564,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 9b1d9f153..8f4be45b8 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) @@ -53,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 { @@ -60,12 +94,6 @@ public class CourseUnitViewModel: ObservableObject { case previous } - struct VerticalData { - var chapterIndex: Int - var sequentialIndex: Int - var verticalIndex: Int - } - var verticals: [CourseVertical] var verticalIndex: Int var courseName: String @@ -95,7 +123,7 @@ public class CourseUnitViewModel: ObservableObject { let chapterIndex: Int let sequentialIndex: Int - var streamingQuality: StreamingQuality { + var streamingQuality: StreamingQuality { storage.userSettings?.streamingQuality ?? .auto } @@ -142,7 +170,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 } @@ -218,11 +246,16 @@ public class CourseUnitViewModel: ObservableObject { // MARK: Navigation to next vertical var nextData: VerticalData? { nextData( - from: VerticalData( - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex, - verticalIndex: verticalIndex - ) + from: currentData + ) + } + + var currentData: VerticalData { + VerticalData( + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex, + verticalIndex: verticalIndex, + blockIndex: index ) } @@ -271,6 +304,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 +325,34 @@ 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, data != currentData else { return } + if let vertical = vertical(for: data), + let block = blockFor(index: data.blockIndex, in: vertical) { router.replaceCourseUnit( courseName: courseName, - blockId: block.id, + blockId: block.blockId, 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 = VerticalData.dataFor(blockId: blockId, in: chapters) else { return } + route(to: data, animated: true) + } + + public var currentCourseId: String { + courseID + } } diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index 17e8be4f6..cdea26e70 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 { @@ -81,7 +81,10 @@ public struct EncodedVideoPlayer: View { .frame(minWidth: playerWidth(for: reader.size)) .cornerRadius(12) .onAppear { - viewModel.controller.player?.play() + if !viewModel.controllerHolder.isPlayingInPip, + !viewModel.controllerHolder.isOtherPlayerInPip { + viewModel.controller.player?.play() + } } if isHorizontal { Spacer() @@ -168,7 +171,9 @@ struct EncodedVideoPlayer_Previews: PreviewProvider { interactor: CourseInteractor(repository: CourseRepositoryMock()), router: CourseRouterMock(), appStorage: CoreStorageMock(), - connectivity: Connectivity() + connectivity: Connectivity(), + pipManager: PipManagerProtocolMock(), + selectedCourseTab: 0 ), isOnScreen: true ) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift index 8d2defbcd..bb5eb8d3e 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift @@ -13,7 +13,10 @@ 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,10 +28,29 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { interactor: CourseInteractorProtocol, router: CourseRouter, appStorage: CoreStorage, - connectivity: ConnectivityProtocol + connectivity: ConnectivityProtocol, + pipManager: PipManagerProtocol, + selectedCourseTab: Int ) { self.url = url + if let holder = pipManager.holder( + for: url, + blockID: blockID, + courseID: courseID, + selectedCourseTab: selectedCourseTab + ) { + controllerHolder = holder + } else { + let holder = PlayerViewControllerHolder( + url: url, + blockID: blockID, + courseID: courseID, + selectedCourseTab: selectedCourseTab + ) + controllerHolder = holder + } + super.init(blockID: blockID, courseID: courseID, languages: languages, @@ -40,9 +62,13 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { playerStateSubject.sink(receiveValue: { [weak self] state in switch state { case .pause: - self?.controller.player?.pause() + if self?.controllerHolder.isPlayingInPip != true { + self?.controller.player?.pause() + } case .kill: - self?.controller.player?.replaceCurrentItem(with: nil) + if self?.controllerHolder.isPlayingInPip != 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..0bb477635 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 { + context.coordinator.currentHolder = playerHolder + if playerHolder.isPlayingInPip { + return playerHolder.playerController + } + + 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,7 @@ 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.isPlayingInPip { let player = context.coordinator.player(from: playerController) player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL!)) player?.currentItem?.preferredMaximumResolution = videoResolution @@ -74,6 +83,8 @@ struct PlayerViewController: UIViewControllerRepresentable { class Coordinator { var currentPlayer: AVPlayer? var observer: Any? + var cancellations: [AnyCancellable] = [] + weak var currentHolder: PlayerViewControllerHolder? func player(from playerController: AVPlayerViewController) -> AVPlayer? { var player = playerController.player @@ -86,9 +97,12 @@ struct PlayerViewController: UIViewControllerRepresentable { } func setPlayer(_ player: AVPlayer?, currentProgress: @escaping ((Float, Double) -> Void)) { + cancellations.removeAll() if let observer = observer { currentPlayer?.removeTimeObserver(observer) - currentPlayer?.pause() + if currentHolder?.isPlayingInPip == false { + currentPlayer?.pause() + } } let interval = CMTime( @@ -105,6 +119,21 @@ struct PlayerViewController: UIViewControllerRepresentable { currentProgress(progress, currentSeconds) } + player?.publisher(for: \.rate) + .sink {[weak self] rate in + guard rate > 0 else { return } + 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 new file mode 100644 index 000000000..3ba64b192 --- /dev/null +++ b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift @@ -0,0 +1,131 @@ +// +// PlayerViewControllerHolder.swift +// Core +// +// Created by Vadim Kuznetsov on 20.03.24. +// + +import AVKit +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 pipRatePublisher() -> AnyPublisher? + func pauseCurrentPipVideo() +} + +#if DEBUG +public class PipManagerProtocolMock: PipManagerProtocol { + public var isPipActive: Bool { + false + } + + public init() {} + public func holder( + for url: URL?, + blockID: String, + courseID: String, + selectedCourseTab: Int + ) -> PlayerViewControllerHolder? { + return nil + } + 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 + +public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegate { + public let url: URL? + public let blockID: String + public let courseID: String + public let selectedCourseTab: Int + 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() + playerController.delegate = self + return playerController + }() + + public init( + url: URL?, + blockID: String, + courseID: String, + selectedCourseTab: Int + ) { + self.url = url + self.blockID = blockID + self.courseID = courseID + 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 + ) { + isPlayingInPip = false + pipManager.remove(holder: self) + } + + public func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { + isPlayingInPip = false + pipManager.remove(holder: self) + } + + public func playerViewControllerRestoreUserInterfaceForPictureInPictureStop( + _ playerViewController: AVPlayerViewController + ) async -> Bool { + do { + try await Container.shared.resolve(PipManagerProtocol.self)?.restore(holder: self) + return true + } catch { + return false + } + } + + 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 + } + + public func pausePipIfNeed() { + if !isPlayingInPip { + pipManager.pauseCurrentPipVideo() + } + } + + public func pipRatePublisher() -> AnyPublisher? { + pipManager.pipRatePublisher() + } +} 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..92bdad530 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: @@ -136,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.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 12b243007..2c94092fc 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 84bb16054..12ee2d514 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -201,6 +201,15 @@ 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)!, + isNestedListEnabled: r.resolve(ConfigProtocol.self)?.uiComponents.courseNestedListEnabled ?? false + ) + }.inObjectScope(.container) } } // swiftlint:enable function_body_length diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 4dbeba697..b0b1768f5 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -187,7 +187,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)!, @@ -324,14 +324,16 @@ 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)! ) } 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, @@ -340,7 +342,9 @@ 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)!, + selectedCourseTab: router.currentCourseTabSelection ) } diff --git a/OpenEdX/Info.plist b/OpenEdX/Info.plist index 9c038b978..2b4cb0751 100644 --- a/OpenEdX/Info.plist +++ b/OpenEdX/Info.plist @@ -29,6 +29,10 @@ UIAppFonts + UIBackgroundModes + + audio + UIViewControllerBasedStatusBarAppearance NSCalendarsUsageDescription diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift index 1ff619e33..ae33a9d64 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift @@ -65,7 +65,6 @@ public protocol DeepLinkRouter: BaseRouter { extension Router: DeepLinkRouter { // MARK: - DeepLinkRouter - public func showDiscoveryDetails( link: DeepLink, pathID: String @@ -324,6 +323,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 new file mode 100644 index 000000000..8720ae03f --- /dev/null +++ b/OpenEdX/Managers/PipManager.swift @@ -0,0 +1,220 @@ +// +// PipManager.swift +// OpenEdX +// +// Created by Vadim Kuznetsov on 20.03.24. +// + +import Course +import Combine +import Discovery +import SwiftUI + +public class PipManager: PipManagerProtocol { + var controllerHolder: PlayerViewControllerHolder? + let discoveryInteractor: DiscoveryInteractorProtocol + let courseInteractor: CourseInteractorProtocol + let router: Router + let isNestedListEnabled: Bool + public var isPipActive: Bool { + controllerHolder != nil + } + + private var ratePublisher: PassthroughSubject? + private var cancellations: [AnyCancellable] = [] + + public init( + router: Router, + discoveryInteractor: DiscoveryInteractorProtocol, + courseInteractor: CourseInteractorProtocol, + isNestedListEnabled: Bool + ) { + self.discoveryInteractor = discoveryInteractor + self.courseInteractor = courseInteractor + self.router = router + self.isNestedListEnabled = isNestedListEnabled + } + + public func holder( + for url: URL?, + blockID: String, + courseID: String, + selectedCourseTab: Int + ) -> PlayerViewControllerHolder? { + if controllerHolder?.blockID == blockID, + controllerHolder?.courseID == courseID, + controllerHolder?.selectedCourseTab == selectedCourseTab { + return controllerHolder + } + + return nil + } + + public func set(holder: PlayerViewControllerHolder) { + controllerHolder = holder + 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 + + // 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 + } + } + // else create navigation stack and push new stack to root navigation controller + 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 + 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 { + 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) + } + + 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 + } + + @MainActor + private 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) + } + 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 + } + + @MainActor + private func containerController( + for holder: PlayerViewControllerHolder + ) async throws -> UIHostingController { + let courseDetails = try await getCourseDetails(for: holder) + let isActive: Bool? = nil + let controller = router.getCourseScreensController( + courseID: courseDetails.courseID, + isActive: isActive, + courseStart: courseDetails.courseStart, + courseEnd: courseDetails.courseEnd, + enrollmentStart: courseDetails.enrollmentStart, + enrollmentEnd: courseDetails.enrollmentEnd, + title: courseDetails.courseTitle + ) + controller.rootView.viewModel.selection = holder.selectedCourseTab + return controller + } + + private 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 + ) + } + } + + private var topCourseUnitController: UIHostingController? { + router.getNavigationController().visibleViewController as? UIHostingController + } +} + +extension PipManager { + enum PipManagerError: Error { + case cantCreateCourseUnitView + case cantCreateCourseVerticalView + } +} diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 6f190a76a..fd728c271 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -321,6 +321,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, @@ -334,8 +353,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( @@ -347,6 +365,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, @@ -361,8 +400,7 @@ public class Router: AuthorizationRouter, title: title ) - let controller = UIHostingController(rootView: screensView) - navigationController.pushViewController(controller, animated: true) + return UIHostingController(rootView: screensView) } public func showHandoutsUpdatesView( @@ -385,12 +423,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, @@ -405,9 +463,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( @@ -444,7 +501,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, @@ -489,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]) }