diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index e5b39bfa01..08d517d03f 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -964,6 +964,7 @@ 2C0AF18C203C67EC000EA3B6 /* MigrationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0AF18B203C67EC000EA3B6 /* MigrationExtensions.swift */; }; 2C1000802029BA7F00E55597 /* BaseCardsStepsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5D51582024653B00B9D932 /* BaseCardsStepsViewController.swift */; }; 2C104B682069064D0026FEB9 /* autocomplete_suggestions.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2C104B672069064D0026FEB9 /* autocomplete_suggestions.plist */; }; + 2C10C477221AB8CC00FA3E13 /* UnitNavigationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C10C476221AB8CC00FA3E13 /* UnitNavigationService.swift */; }; 2C1219901F9655AB00A43E98 /* NotificationsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C12198F1F9655AB00A43E98 /* NotificationsSection.swift */; }; 2C130E892125A3F50022E998 /* AssemblyFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C130E882125A3F50022E998 /* AssemblyFactoryMock.swift */; }; 2C130E8B2125A4410022E998 /* ApplicationAssemblyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C130E8A2125A4410022E998 /* ApplicationAssemblyMock.swift */; }; @@ -2630,9 +2631,9 @@ 2C8A0F2220FC7CD80009F67E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 08A1257B1BDEBCC90066B2B2 /* Localizable.strings */; }; 2C8A0F3320FCCE230009F67E /* VKSocialSDKProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088E58C31DE34ED20009B9CE /* VKSocialSDKProvider.swift */; }; 2C8A0F3620FCCE650009F67E /* SocialSDKProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088E58C01DE34E2F0009B9CE /* SocialSDKProvider.swift */; }; - 2C8AD7662208646100C9C089 /* DownloadsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0889FEAE1BFB771200C6417E /* DownloadsViewController.swift */; }; 2C8AD758220311F000C9C089 /* DataBackUpdateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8AD757220311F000C9C089 /* DataBackUpdateService.swift */; }; 2C8AD7622204932300C9C089 /* CourseSubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8AD7612204932200C9C089 /* CourseSubscriptionManager.swift */; }; + 2C8AD7662208646100C9C089 /* DownloadsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0889FEAE1BFB771200C6417E /* DownloadsViewController.swift */; }; 2C8C96EA211DE34700A9FB99 /* RateAppViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 089056101E98021000B8FE6A /* RateAppViewController.xib */; }; 2C8CB0E31FB48F39008CB1AC /* EnrollmentsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8CB0E21FB48F39008CB1AC /* EnrollmentsAPI.swift */; }; 2C905230212E9C9300354DFB /* CoursePlainObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C90522F212E9C9300354DFB /* CoursePlainObjectTests.swift */; }; @@ -5570,6 +5571,7 @@ 2C09313621D00B8D002B605D /* VideoDownloadingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadingService.swift; sourceTree = "<group>"; }; 2C0AF18B203C67EC000EA3B6 /* MigrationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationExtensions.swift; sourceTree = "<group>"; }; 2C104B672069064D0026FEB9 /* autocomplete_suggestions.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = autocomplete_suggestions.plist; sourceTree = "<group>"; }; + 2C10C476221AB8CC00FA3E13 /* UnitNavigationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitNavigationService.swift; sourceTree = "<group>"; }; 2C12198F1F9655AB00A43E98 /* NotificationsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsSection.swift; sourceTree = "<group>"; }; 2C130E882125A3F50022E998 /* AssemblyFactoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssemblyFactoryMock.swift; sourceTree = "<group>"; }; 2C130E8A2125A4410022E998 /* ApplicationAssemblyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationAssemblyMock.swift; sourceTree = "<group>"; }; @@ -9914,6 +9916,7 @@ 089877B121404CF00065DFA2 /* StringStorageServiceProtocol.swift */, 2C23C12621AD8907001DCF89 /* NextLessonService.swift */, 2C8AD757220311F000C9C089 /* DataBackUpdateService.swift */, + 2C10C476221AB8CC00FA3E13 /* UnitNavigationService.swift */, ); path = Services; sourceTree = "<group>"; @@ -15459,6 +15462,7 @@ 73DB05040F3345F6568DCCBB /* FullscreenCourseListView.swift in Sources */, 62E983594B255BB6BE92AD8A /* UniqueIdentifierType.swift in Sources */, 62E9850F9C459CC122A416F3 /* ContentLanguageSwitchAvailabilityService.swift in Sources */, + 2C10C477221AB8CC00FA3E13 /* UnitNavigationService.swift in Sources */, 62E9870883D5C6A1DCB15E4B /* TagsOutputProtocol.swift in Sources */, 62E9893ECE0FA9E7AEC2B6DD /* CourseListCollectionOutputProtocol.swift in Sources */, 62E98236342BF7C367FB6F36 /* Array+reordering.swift in Sources */, diff --git a/Stepic/LessonPresenter.swift b/Stepic/LessonPresenter.swift index 1b42ccaa46..88ac498136 100644 --- a/Stepic/LessonPresenter.swift +++ b/Stepic/LessonPresenter.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +import SVProgressHUD enum LessonViewState { case displayingSteps, placeholder, refreshing @@ -28,6 +29,8 @@ class LessonPresenter { static let stepUpdatedNotification = "StepUpdatedNotification" fileprivate var tabViewsForStepId = [Int: UIView]() + private var controllerForIndex: [Int: UIViewController] = [:] + fileprivate var lesson: Lesson? fileprivate var startStepId: Int = 0 fileprivate var stepId: Int? @@ -37,9 +40,6 @@ class LessonPresenter { var stepsAPI: StepsAPI var lessonsAPI: LessonsAPI - var shouldNavigateToPrev: Bool = false - var shouldNavigateToNext: Bool = false - fileprivate var didInitSteps: Bool = false fileprivate var didSelectTab: Bool = false @@ -55,6 +55,24 @@ class LessonPresenter { return service }() + private lazy var unitNavigationService: UnitNavigationServiceProtocol = { + let service = UnitNavigationService( + sectionsPersistenceService: SectionsPersistenceService(), + sectionsNetworkService: SectionsNetworkService(sectionsAPI: SectionsAPI()), + unitsPersistenceService: UnitsPersistenceService(), + unitsNetworkService: UnitsNetworkService(unitsAPI: UnitsAPI()), + coursesPersistenceService: CoursesPersistenceService(), + coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()) + ) + return service + }() + + private var didNextUnitLoad = false + private var nextUnit: Unit? + + private var didPreviousUnitLoad = false + private var previousUnit: Unit? + init(objects: LessonInitObjects?, ids: LessonInitIds?, stepsAPI: StepsAPI, lessonsAPI: LessonsAPI) { if let objects = objects { self.lesson = objects.lesson @@ -76,6 +94,56 @@ class LessonPresenter { name: .stepDone, object: nil ) + + if let unitID = self.unitId { + DispatchQueue.global().async { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.unitNavigationService.findUnitForNavigation( + from: unitID, + direction: .next + ).done { unit in + print("next unit loaded, unit = \(unit?.id)") + guard let unit = unit else { + return + } + + if let stepsCount = strongSelf.lesson?.stepsArray.count { + (strongSelf.controllerForIndex[stepsCount - 1] as? VideoStepViewController)?.nextLessonHandler = { + strongSelf.navigateToNextOrPreviousUnit(direction: .next) + } + (strongSelf.controllerForIndex[stepsCount - 1] as? WebStepViewController)?.nextLessonHandler = { + strongSelf.navigateToNextOrPreviousUnit(direction: .next) + } + } + + strongSelf.didNextUnitLoad = true + strongSelf.nextUnit = unit + }.cauterize() + + strongSelf.unitNavigationService.findUnitForNavigation( + from: unitID, + direction: .previous + ).done { unit in + print("previous unit loaded, unit = \(unit?.id)") + guard let unit = unit else { + return + } + + (strongSelf.controllerForIndex[0] as? VideoStepViewController)?.prevLessonHandler = { + strongSelf.navigateToNextOrPreviousUnit(direction: .previous) + } + (strongSelf.controllerForIndex[0] as? WebStepViewController)?.prevLessonHandler = { + strongSelf.navigateToNextOrPreviousUnit(direction: .previous) + } + + strongSelf.didPreviousUnitLoad = true + strongSelf.previousUnit = unit + }.cauterize() + } + } } deinit { @@ -165,14 +233,6 @@ class LessonPresenter { return } - if let section = lesson?.unit?.section, - let unitId = unitId { - if let index = section.unitsArray.index(of: unitId) { - shouldNavigateToPrev = shouldNavigateToPrev || (index != 0) - shouldNavigateToNext = shouldNavigateToNext || (index < section.unitsArray.count - 1) - } - } - view?.updateTitle(title: lesson?.title ?? NSLocalizedString("Lesson", comment: "")) if let stepId = stepId { @@ -298,26 +358,25 @@ class LessonPresenter { self?.canSendViews ?? false } - if context == .unit { - if index == 0 && shouldNavigateToPrev { - stepController.prevLessonHandler = { - [weak self] in - if let unitID = self?.unitId { - self?.sectionNavigationDelegate?.didRequestPreviousUnitPresentationForLessonInUnit(unitID: unitID) - } + if context == .unit && index == 0 && didPreviousUnitLoad { + stepController.prevLessonHandler = { [weak self] in + guard let strongSelf = self else { + return } + strongSelf.navigateToNextOrPreviousUnit(direction: .previous) } + } - if index == lesson.steps.count - 1 && shouldNavigateToNext { - stepController.nextLessonHandler = { - [weak self] in - if let unitID = self?.unitId { - self?.sectionNavigationDelegate?.didRequestNextUnitPresentationForLessonInUnit(unitID: unitID) - } + if context == .unit && index == lesson.stepsArray.count - 1 && didNextUnitLoad { + stepController.nextLessonHandler = { [weak self] in + guard let strongSelf = self else { + return } + strongSelf.navigateToNextOrPreviousUnit(direction: .next) } } + controllerForIndex[index] = stepController return stepController } else { let stepController = ControllerHelper.instantiateViewController(identifier: "WebStepViewController") as! WebStepViewController @@ -345,30 +404,83 @@ class LessonPresenter { self?.canSendViews ?? false } stepController.lessonSlug = lesson.slug - if context == .unit { - if index == 0 && shouldNavigateToPrev { - stepController.prevLessonHandler = { - [weak self] in - if let unitID = self?.unitId { - self?.sectionNavigationDelegate?.didRequestPreviousUnitPresentationForLessonInUnit(unitID: unitID) - } + + if context == .unit && index == 0 && didPreviousUnitLoad { + stepController.prevLessonHandler = { [weak self] in + guard let strongSelf = self else { + return } + strongSelf.navigateToNextOrPreviousUnit(direction: .previous) } + } - if index == lesson.steps.count - 1 && shouldNavigateToNext { - stepController.nextLessonHandler = { - [weak self] in - if let unitID = self?.unitId { - self?.sectionNavigationDelegate?.didRequestNextUnitPresentationForLessonInUnit(unitID: unitID) - } + if context == .unit && index == lesson.stepsArray.count - 1 && didNextUnitLoad { + stepController.nextLessonHandler = { [weak self] in + guard let strongSelf = self else { + return } + strongSelf.navigateToNextOrPreviousUnit(direction: .next) } } + controllerForIndex[index] = stepController return stepController } } + private func navigateToNextOrPreviousUnit(direction: UnitNavigationDirection) { + guard let targetUnit = direction == .next ? self.nextUnit : self.previousUnit else { + return + } + + SVProgressHUD.show() + let cachedLesson = Lesson.fetch([targetUnit.lessonId]).first + if let lesson = cachedLesson { + self.replaceByNewLesson( + lesson: lesson, + unit: targetUnit, + stepArrayFunction: { direction == .next ? $0.first : $0.last } + ) + } else { + self.lessonsAPI.retrieve(ids: [targetUnit.lessonId]).done { lessons in + guard let lesson = lessons.first else { + SVProgressHUD.showError(withStatus: nil) + return + } + + self.replaceByNewLesson( + lesson: lesson, + unit: targetUnit, + stepArrayFunction: { direction == .next ? $0.first : $0.last } + ) + SVProgressHUD.dismiss() + }.catch { _ in + print("error while fetching lesson for next/prev unit") + SVProgressHUD.showError(withStatus: nil) + } + } + } + + private func replaceByNewLesson(lesson: Lesson, unit: Unit, stepArrayFunction: ([Int]) -> Int?) { + guard let viewControllers = self.view?.nController?.viewControllers, + let presentingViewController = self.view?.nController?.viewControllers[safe: viewControllers.count - 2] else { + return + } + + guard let stepID = stepArrayFunction(lesson.stepsArray) else { + SVProgressHUD.showError(withStatus: nil) + return + } + + let newLessonController = LessonLegacyAssembly( + initObjects: nil, + initIDs: (stepId: stepID, unitId: unit.id) + ).makeModule() + + SVProgressHUD.dismiss() + presentingViewController.replace(by: newLessonController) + } + func tabView(index: Int) -> UIView { guard lesson != nil else { return UIView() diff --git a/Stepic/LessonViewController.swift b/Stepic/LessonViewController.swift index 3058f659a2..10009b1159 100644 --- a/Stepic/LessonViewController.swift +++ b/Stepic/LessonViewController.swift @@ -13,19 +13,13 @@ import SnapKit final class LessonLegacyAssembly: Assembly { private let initObjects: LessonInitObjects? private let initIDs: LessonInitIds? - private let navigationRules: LessonNavigationRules - private let navigationDelegate: SectionNavigationDelegate init( initObjects: LessonInitObjects?, - initIDs: LessonInitIds?, - navigationRules: LessonNavigationRules, - navigationDelegate: SectionNavigationDelegate + initIDs: LessonInitIds? ) { self.initObjects = initObjects self.initIDs = initIDs - self.navigationRules = navigationRules - self.navigationDelegate = navigationDelegate } func makeModule() -> UIViewController { @@ -37,9 +31,6 @@ final class LessonLegacyAssembly: Assembly { lessonVC.initObjects = self.initObjects lessonVC.initIds = self.initIDs - lessonVC.navigationRules = self.navigationRules - lessonVC.sectionNavigationDelegate = self.navigationDelegate - return lessonVC } } @@ -50,10 +41,6 @@ class LessonViewController: PagerController, ShareableController, LessonView { var parentShareBlock: ((UIActivityViewController) -> Void)? - weak var sectionNavigationDelegate: SectionNavigationDelegate? - - var navigationRules : LessonNavigationRules? - fileprivate var presenter: LessonPresenter? lazy var activityView: UIView = self.initActivityView() @@ -145,11 +132,6 @@ class LessonViewController: PagerController, ShareableController, LessonView { presenter = LessonPresenter(objects: initObjects, ids: initIds, stepsAPI: ApiDataDownloader.steps, lessonsAPI: ApiDataDownloader.lessons) presenter?.view = self - presenter?.sectionNavigationDelegate = sectionNavigationDelegate - if let rules = navigationRules { - presenter?.shouldNavigateToPrev = rules.prev - presenter?.shouldNavigateToNext = rules.next - } presenter?.refreshSteps() } diff --git a/Stepic/Modules/CourseInfo/CourseInfoDataFlow.swift b/Stepic/Modules/CourseInfo/CourseInfoDataFlow.swift index 2bcb1c49ab..624fde1d1f 100644 --- a/Stepic/Modules/CourseInfo/CourseInfoDataFlow.swift +++ b/Stepic/Modules/CourseInfo/CourseInfoDataFlow.swift @@ -48,16 +48,12 @@ enum CourseInfo { struct Response { let lesson: Lesson let unitID: Unit.IdType - let navigationRules: LessonNavigationRules - let navigationDelegate: SectionNavigationDelegate } @available(*, deprecated, message: "Old ugly Lesson controller initialization") struct ViewModel { let initObjects: LessonInitObjects let initIDs: LessonInitIds - let navigationRules: LessonNavigationRules - let navigationDelegate: SectionNavigationDelegate } } diff --git a/Stepic/Modules/CourseInfo/CourseInfoInteractor.swift b/Stepic/Modules/CourseInfo/CourseInfoInteractor.swift index c886f5ea05..d51ba24745 100644 --- a/Stepic/Modules/CourseInfo/CourseInfoInteractor.swift +++ b/Stepic/Modules/CourseInfo/CourseInfoInteractor.swift @@ -261,23 +261,12 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { } extension CourseInfoInteractor: CourseInfoTabSyllabusOutputProtocol { - func presentLesson( - in unit: Unit, - navigationDelegate: SectionNavigationDelegate, - navigationRules: LessonNavigationRules - ) { + func presentLesson(in unit: Unit) { guard let lesson = unit.lesson else { return } - self.presenter.presentLesson( - response: .init( - lesson: lesson, - unitID: unit.id, - navigationRules: navigationRules, - navigationDelegate: navigationDelegate - ) - ) + self.presenter.presentLesson(response: .init(lesson: lesson, unitID: unit.id)) } func presentPersonalDeadlinesCreation(for course: Course) { diff --git a/Stepic/Modules/CourseInfo/CourseInfoPresenter.swift b/Stepic/Modules/CourseInfo/CourseInfoPresenter.swift index 280866f5f9..9d99b99645 100644 --- a/Stepic/Modules/CourseInfo/CourseInfoPresenter.swift +++ b/Stepic/Modules/CourseInfo/CourseInfoPresenter.swift @@ -50,9 +50,7 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { let viewModel = CourseInfo.ShowLesson.ViewModel( initObjects: initObjects, - initIDs: initIDs, - navigationRules: response.navigationRules, - navigationDelegate: response.navigationDelegate + initIDs: initIDs ) self.viewController?.displayLesson(viewModel: viewModel) diff --git a/Stepic/Modules/CourseInfo/CourseInfoViewController.swift b/Stepic/Modules/CourseInfo/CourseInfoViewController.swift index 3fb43e816e..2fc3ae4742 100644 --- a/Stepic/Modules/CourseInfo/CourseInfoViewController.swift +++ b/Stepic/Modules/CourseInfo/CourseInfoViewController.swift @@ -351,18 +351,10 @@ extension CourseInfoViewController: CourseInfoViewControllerProtocol { func displayLesson(viewModel: CourseInfo.ShowLesson.ViewModel) { let assembly = LessonLegacyAssembly( initObjects: viewModel.initObjects, - initIDs: viewModel.initIDs, - navigationRules: viewModel.navigationRules, - navigationDelegate: viewModel.navigationDelegate + initIDs: viewModel.initIDs ) - // If already present lesson then replace top controller - let shouldReplaceLesson = self.navigationController?.topViewController !== self - if shouldReplaceLesson { - self.replace(by: assembly.makeModule()) - } else { - self.push(module: assembly.makeModule()) - } + self.push(module: assembly.makeModule()) } func displayPersonalDeadlinesSettings(viewModel: CourseInfo.PersonalDeadlinesSettings.ViewModel) { diff --git a/Stepic/Modules/CourseInfoTabInfo/Provider/SectionsNetworkService.swift b/Stepic/Modules/CourseInfoTabInfo/Provider/SectionsNetworkService.swift index 1f0e543dfa..de036a7994 100644 --- a/Stepic/Modules/CourseInfoTabInfo/Provider/SectionsNetworkService.swift +++ b/Stepic/Modules/CourseInfoTabInfo/Provider/SectionsNetworkService.swift @@ -11,6 +11,7 @@ import PromiseKit protocol SectionsNetworkServiceProtocol: class { func fetch(ids: [Section.IdType]) -> Promise<[Section]> + func fetch(id: Section.IdType) -> Promise<Section?> } final class SectionsNetworkService: SectionsNetworkServiceProtocol { @@ -31,6 +32,12 @@ final class SectionsNetworkService: SectionsNetworkServiceProtocol { } } + func fetch(id: Section.IdType) -> Promise<Section?> { + return self.fetch(ids: [id]).then { result -> Promise<Section?> in + Promise.value(result.first) + } + } + enum Error: Swift.Error { case fetchFailed } diff --git a/Stepic/Modules/CourseInfoTabInfo/Provider/SectionsPersistenceService.swift b/Stepic/Modules/CourseInfoTabInfo/Provider/SectionsPersistenceService.swift index 791be22c76..39c34fae95 100644 --- a/Stepic/Modules/CourseInfoTabInfo/Provider/SectionsPersistenceService.swift +++ b/Stepic/Modules/CourseInfoTabInfo/Provider/SectionsPersistenceService.swift @@ -11,6 +11,7 @@ import PromiseKit protocol SectionsPersistenceServiceProtocol: class { func fetch(ids: [Section.IdType]) -> Promise<[Section]> + func fetch(id: Section.IdType) -> Promise<Section?> } final class SectionsPersistenceService: SectionsPersistenceServiceProtocol { @@ -25,6 +26,16 @@ final class SectionsPersistenceService: SectionsPersistenceServiceProtocol { } } + func fetch(id: Section.IdType) -> Promise<Section?> { + return Promise { seal in + Section.fetchAsync(ids: [id]).done { sections in + seal.fulfill(sections.first) + }.catch { _ in + seal.reject(Error.fetchFailed) + } + } + } + enum Error: Swift.Error { case fetchFailed } diff --git a/Stepic/Modules/CourseInfoTabInfo/Provider/UnitsNetworkService.swift b/Stepic/Modules/CourseInfoTabInfo/Provider/UnitsNetworkService.swift index 1ffcaedd63..5cea29aed3 100644 --- a/Stepic/Modules/CourseInfoTabInfo/Provider/UnitsNetworkService.swift +++ b/Stepic/Modules/CourseInfoTabInfo/Provider/UnitsNetworkService.swift @@ -11,6 +11,7 @@ import PromiseKit protocol UnitsNetworkServiceProtocol: class { func fetch(ids: [Unit.IdType]) -> Promise<[Unit]> + func fetch(id: Unit.IdType) -> Promise<Unit?> } final class UnitsNetworkService: UnitsNetworkServiceProtocol { @@ -31,6 +32,12 @@ final class UnitsNetworkService: UnitsNetworkServiceProtocol { } } + func fetch(id: Unit.IdType) -> Promise<Unit?> { + return self.fetch(ids: [id]).then { result -> Promise<Unit?> in + Promise.value(result.first) + } + } + enum Error: Swift.Error { case fetchFailed } diff --git a/Stepic/Modules/CourseInfoTabInfo/Provider/UnitsPersistenceService.swift b/Stepic/Modules/CourseInfoTabInfo/Provider/UnitsPersistenceService.swift index 1456d5f883..bd8b61e237 100644 --- a/Stepic/Modules/CourseInfoTabInfo/Provider/UnitsPersistenceService.swift +++ b/Stepic/Modules/CourseInfoTabInfo/Provider/UnitsPersistenceService.swift @@ -10,7 +10,8 @@ import Foundation import PromiseKit protocol UnitsPersistenceServiceProtocol: class { - func fetch(ids: [Unit.IdType])-> Promise<[Unit]> + func fetch(ids: [Unit.IdType]) -> Promise<[Unit]> + func fetch(id: Unit.IdType) -> Promise<Unit?> } final class UnitsPersistenceService: UnitsPersistenceServiceProtocol { @@ -25,6 +26,16 @@ final class UnitsPersistenceService: UnitsPersistenceServiceProtocol { } } + func fetch(id: Unit.IdType) -> Promise<Unit?> { + return Promise { seal in + Unit.fetchAsync(ids: [id]).done { units in + seal.fulfill(units.first) + }.catch { _ in + seal.reject(Error.fetchFailed) + } + } + } + enum Error: Swift.Error { case fetchFailed } diff --git a/Stepic/Modules/CourseInfoTabSyllabus/CourseInfoTabSyllabusInteractor.swift b/Stepic/Modules/CourseInfoTabSyllabus/CourseInfoTabSyllabusInteractor.swift index 622b8ec68c..f32b2c4e4b 100644 --- a/Stepic/Modules/CourseInfoTabSyllabus/CourseInfoTabSyllabusInteractor.swift +++ b/Stepic/Modules/CourseInfoTabSyllabus/CourseInfoTabSyllabusInteractor.swift @@ -427,14 +427,7 @@ final class CourseInfoTabSyllabusInteractor: CourseInfoTabSyllabusInteractorProt return } - self.moduleOutput?.presentLesson( - in: unit, - navigationDelegate: self, - navigationRules: ( - prev: self.nextLessonService.findPreviousUnit(for: unit) != nil, - next: self.nextLessonService.findNextUnit(for: unit) != nil - ) - ) + self.moduleOutput?.presentLesson(in: unit) } enum Error: Swift.Error { diff --git a/Stepic/Modules/CourseInfoTabSyllabus/CourseInfoTabSyllabusOutputProtocol.swift b/Stepic/Modules/CourseInfoTabSyllabus/CourseInfoTabSyllabusOutputProtocol.swift index 40a0941842..6c6b27d4c4 100644 --- a/Stepic/Modules/CourseInfoTabSyllabus/CourseInfoTabSyllabusOutputProtocol.swift +++ b/Stepic/Modules/CourseInfoTabSyllabus/CourseInfoTabSyllabusOutputProtocol.swift @@ -9,11 +9,7 @@ import Foundation protocol CourseInfoTabSyllabusOutputProtocol: class { - func presentLesson( - in unit: Unit, - navigationDelegate: SectionNavigationDelegate, - navigationRules: LessonNavigationRules - ) + func presentLesson(in unit: Unit) func presentExamLesson() func presentPersonalDeadlinesCreation(for course: Course) func presentPersonalDeadlinesSettings(for course: Course) diff --git a/Stepic/Services/UnitNavigationService.swift b/Stepic/Services/UnitNavigationService.swift new file mode 100644 index 0000000000..2129cf4756 --- /dev/null +++ b/Stepic/Services/UnitNavigationService.swift @@ -0,0 +1,232 @@ +// +// UnitNavigationService.swift +// Stepic +// +// Created by Vladislav Kiryukhin on 18/02/2019. +// Copyright © 2019 Alex Karpov. All rights reserved. +// + +import Foundation +import PromiseKit + +enum UnitNavigationDirection { + case next + case previous +} + +protocol UnitNavigationServiceProtocol: class { + func findUnitForNavigation( + from unit: Unit.IdType, + direction: UnitNavigationDirection + ) -> Promise<Unit?> +} + +final class UnitNavigationService: UnitNavigationServiceProtocol { + private let sectionsPersistenceService: SectionsPersistenceServiceProtocol + private let sectionsNetworkService: SectionsNetworkServiceProtocol + private let unitsPersistenceService: UnitsPersistenceServiceProtocol + private let unitsNetworkService: UnitsNetworkServiceProtocol + private let coursesPersistenceService: CoursesPersistenceServiceProtocol + private let coursesNetworkService: CoursesNetworkServiceProtocol + + init( + sectionsPersistenceService: SectionsPersistenceServiceProtocol, + sectionsNetworkService: SectionsNetworkServiceProtocol, + unitsPersistenceService: UnitsPersistenceServiceProtocol, + unitsNetworkService: UnitsNetworkServiceProtocol, + coursesPersistenceService: CoursesPersistenceServiceProtocol, + coursesNetworkService: CoursesNetworkServiceProtocol + ) { + self.sectionsPersistenceService = sectionsPersistenceService + self.sectionsNetworkService = sectionsNetworkService + self.unitsPersistenceService = unitsPersistenceService + self.unitsNetworkService = unitsNetworkService + self.coursesPersistenceService = coursesPersistenceService + self.coursesNetworkService = coursesNetworkService + } + + func findUnitForNavigation( + from unit: Unit.IdType, + direction: UnitNavigationDirection + ) -> Promise<Unit?> { + return self.getUnitFromCacheOrNetwork(id: unit).then { unit -> Promise<(Unit?, Section?)> in + guard let unit = unit else { + return Promise.value((nil, nil)) + } + + return self.getSectionFromCacheOrNetwork(id: unit.sectionId).map { (unit, $0) } + }.then { unit, section -> Promise<Unit?> in + guard let section = section, let unit = unit else { + return Promise.value(nil) + } + + unit.section = section + CoreDataHelper.instance.save() + + // Cause unit & section have 1-indexed position in API + let unitPosition = unit.position - 1 + let sectionPosition = section.position - 1 + + let shouldLookUpInPreviousSection = unitPosition == 0 + && direction == .previous + let shouldLookUpInNextSection = unitPosition == (section.unitsArray.count - 1) + && direction == .next + if shouldLookUpInPreviousSection || shouldLookUpInNextSection { + return self.findUnitInAnotherSections( + courseID: section.courseId, + sectionPosition: sectionPosition, + direction: direction + ) + } else { + return self.findUnitInCurrentSection( + section, + unitPosition: unitPosition, + direction: direction + ) + } + } + } + + private func findUnitInCurrentSection( + _ section: Section, + unitPosition: Int, + direction: UnitNavigationDirection + ) -> Promise<Unit?> { + let unitID: Unit.IdType? = { + switch direction { + case .next: + return section.unitsArray[safe: unitPosition + 1] + case .previous: + return section.unitsArray[safe: unitPosition - 1] + } + }() + + guard let targetUnitID = unitID else { + return Promise.value(nil) + } + + return self.getUnitFromCacheOrNetwork(id: targetUnitID) + } + + private func findUnitInAnotherSections( + courseID: Course.IdType, + sectionPosition: Int, + direction: UnitNavigationDirection + ) -> Promise<Unit?> { + return self.getSlicedSections( + courseID: courseID, + sectionPosition: sectionPosition, + direction: direction + ).then { sections -> Promise<Section?> in + let sections = direction == .previous ? sections.reversed() : sections + for section in sections { + if section.isReachable, + !section.isExam, + section.unitsArray.count > 0 { + return Promise.value(section) + } + } + + return Promise.value(nil) + }.then { targetSection -> Promise<Unit?> in + guard let section = targetSection else { + return Promise.value(nil) + } + + guard let targetUnitID = direction == .previous + ? section.unitsArray.last + : section.unitsArray.first else { + return Promise.value(nil) + } + + // Load all units to make next findUnitInCurrentSection calls faster + let allUnitsFromCacheOrNetwork = section.unitsArray.map { unitID in + self.getUnitFromCacheOrNetwork(id: unitID) + } + + return Promise { seal in + when(fulfilled: allUnitsFromCacheOrNetwork).done { units in + let targetUnit = units.compactMap { $0 } + .filter { $0.id == targetUnitID } + .first + seal.fulfill(targetUnit) + }.catch { error in + seal.reject(error) + } + } + } + } + + /// Return array of sections after or before section with given position + private func getSlicedSections( + courseID: Course.IdType, + sectionPosition: Int, + direction: UnitNavigationDirection + ) -> Promise<[Section]> { + return Promise { seal in + self.getCourseFromCacheOrNetwork(id: courseID).then { course -> Promise<[Section]> in + guard let course = course else { + throw Error.unknownCourse + } + + let sectionIDs: [Section.IdType] = { + switch direction { + case .next: + return sectionPosition == course.sectionsArray.count - 1 + ? [] + : Array(course.sectionsArray[(sectionPosition + 1)...]) + case .previous: + return sectionPosition == 0 + ? [] + : Array(course.sectionsArray[...(sectionPosition - 1)]) + } + }() + + return self.sectionsNetworkService.fetch(ids: sectionIDs) + }.done { sections in + seal.fulfill(sections) + }.catch { error in + seal.reject(error) + } + } + } + + // MARK: Helpers + // Remove after network layer & services refactoring + + private func getUnitFromCacheOrNetwork(id: Unit.IdType) -> Promise<Unit?> { + return self.unitsPersistenceService.fetch(id: id).then { unit -> Promise<Unit?> in + if let unit = unit { + return Promise.value(unit) + } else { + return self.unitsNetworkService.fetch(id: id) + } + } + } + + private func getCourseFromCacheOrNetwork(id: Course.IdType) -> Promise<Course?> { + return self.coursesPersistenceService.fetch(id: id).then { course -> Promise<Course?> in + if let course = course { + return Promise.value(course) + } else { + return self.coursesNetworkService.fetch(id: id) + } + } + } + + private func getSectionFromCacheOrNetwork(id: Section.IdType) -> Promise<Section?> { + return self.sectionsPersistenceService.fetch(id: id).then { section -> Promise<Section?> in + if let section = section { + return Promise.value(section) + } else { + return self.sectionsNetworkService.fetch(id: id) + } + } + } + + // MARK: Enums + + enum Error: Swift.Error { + case unknownCourse + } +} diff --git a/Stepic/VideoStepViewController.swift b/Stepic/VideoStepViewController.swift index e772fda932..c08ba2e797 100644 --- a/Stepic/VideoStepViewController.swift +++ b/Stepic/VideoStepViewController.swift @@ -24,8 +24,16 @@ class VideoStepViewController: UIViewController { var assignment: Assignment? - var nextLessonHandler: (() -> Void)? - var prevLessonHandler: (() -> Void)? + var nextLessonHandler: (() -> Void)? { + didSet { + refreshNextPrevButtons() + } + } + var prevLessonHandler: (() -> Void)? { + didSet { + refreshNextPrevButtons() + } + } var nController: UINavigationController? var nItem: UINavigationItem! @@ -63,6 +71,7 @@ class VideoStepViewController: UIViewController { prevLessonButton.setTitle(" \(NSLocalizedString("PrevLesson", comment: "")) ", for: UIControlState()) initialize() + refreshNextPrevButtons() navigationController?.navigationBar.sizeToFit() } @@ -96,22 +105,16 @@ class VideoStepViewController: UIViewController { discussionCountViewHeight.constant = 0 } - if nextLessonHandler == nil { - nextLessonButton.isHidden = true - } else { - nextLessonButton.setStepicWhiteStyle() - } + nextLessonButton.setStepicWhiteStyle() + prevLessonButton.setStepicWhiteStyle() + } - if prevLessonHandler == nil { - prevLessonButton.isHidden = true - } else { - prevLessonButton.setStepicWhiteStyle() + func refreshNextPrevButtons() { + if nextLessonButton != nil { + nextLessonButton.isHidden = nextLessonHandler == nil } - - if nextLessonHandler == nil && prevLessonHandler == nil { - nextLessonButtonHeight.constant = 0 - prevLessonButtonHeight.constant = 0 - prevNextLessonButtonsContainerViewHeight.constant = 0 + if prevLessonButton != nil { + prevLessonButton.isHidden = prevLessonHandler == nil } } diff --git a/Stepic/WebStepViewController.swift b/Stepic/WebStepViewController.swift index a572aaabd5..48c820a805 100644 --- a/Stepic/WebStepViewController.swift +++ b/Stepic/WebStepViewController.swift @@ -32,8 +32,16 @@ class WebStepViewController: UIViewController { @IBOutlet weak var prevToBottomDistance: NSLayoutConstraint! @IBOutlet weak var nextToBottomDistance: NSLayoutConstraint! - var nextLessonHandler: (() -> Void)? - var prevLessonHandler: (() -> Void)? + var nextLessonHandler: (() -> Void)? { + didSet { + refreshNextPrevButtons() + } + } + var prevLessonHandler: (() -> Void)? { + didSet { + refreshNextPrevButtons() + } + } weak var lessonView: LessonView? @@ -78,6 +86,7 @@ class WebStepViewController: UIViewController { prevLessonButton.setTitle(" \(NSLocalizedString("PrevLesson", comment: "")) ", for: UIControlState()) initialize() + refreshNextPrevButtons() } @objc func sharePressed(_ item: UIBarButtonItem) { @@ -109,25 +118,16 @@ class WebStepViewController: UIViewController { discussionCountViewHeight.constant = 0 } - if nextLessonHandler == nil { - nextLessonButton.isHidden = true - } else { - nextLessonButton.setStepicWhiteStyle() - } + nextLessonButton.setStepicWhiteStyle() + prevLessonButton.setStepicWhiteStyle() + } - if prevLessonHandler == nil { - prevLessonButton.isHidden = true - } else { - prevLessonButton.setStepicWhiteStyle() + func refreshNextPrevButtons() { + if nextLessonButton != nil { + nextLessonButton.isHidden = nextLessonHandler == nil } - - if nextLessonHandler == nil && prevLessonHandler == nil { - nextLessonButtonHeight.constant = 0 - prevLessonButtonHeight.constant = 0 - discussionToNextDistance.constant = 0 - discussionToPrevDistance.constant = 0 - prevToBottomDistance.constant = 0 - nextToBottomDistance.constant = 0 + if prevLessonButton != nil { + prevLessonButton.isHidden = prevLessonHandler == nil } }