diff --git a/Domain/UseCases/CurriculumsUseCase.swift b/Domain/UseCases/CurriculumsUseCase.swift index c4d2522..712a7ce 100644 --- a/Domain/UseCases/CurriculumsUseCase.swift +++ b/Domain/UseCases/CurriculumsUseCase.swift @@ -11,5 +11,5 @@ import RxSwift public protocol CurriculumsUseCase { func semesters(targetStudentId: String) -> Observable<[Semester]> - func courses(targetStudentId: String, year: String, semester: String) -> Observable + func courses(targetStudentId: String, year: String, semester: String) -> Observable<[[Course]]> } diff --git a/NetworkPlatform/UseCases/CurriculumsUseCase.swift b/NetworkPlatform/UseCases/CurriculumsUseCase.swift index daaaec9..a1e4dc4 100644 --- a/NetworkPlatform/UseCases/CurriculumsUseCase.swift +++ b/NetworkPlatform/UseCases/CurriculumsUseCase.swift @@ -24,11 +24,53 @@ final class CurriculumsUseCase: Domain.CurriculumsUseCase { .map([Domain.Semester].self) } - func courses(targetStudentId: String, year: String, semester: String) -> Observable { - return provider.rx.request(.courses(targetStudentId: targetStudentId, year: year, semester: semester)) + func courses(targetStudentId: String, year: String, semester: String) -> Observable<[[Domain.Course]]> { + return provider.rx.request(.courses(targetStudentId: targetStudentId, + year: year, + semester: semester)) .asObservable() .filterSuccessfulStatusCodes() .map(Domain.CurriculumCourses.self) + .map { [weak self] (curriculumCourses) -> [[Domain.Course]] in + guard let self = self else { return [] } + return self.generateCourses(from: curriculumCourses) + } + } + + private func generateCourses(from curriculumCourses: Domain.CurriculumCourses) -> [[Domain.Course]] { + var array: [[Domain.Course]] = self.initCourses() + for course in curriculumCourses.courses { + for (day, period) in course.periods.enumerated() { + self.reshapCourses(period: period, + day: day, + course: course, + array: &array) + } + } + return array + } + + private func reshapCourses(period: String, day: Int, course: Course, array: inout[[Domain.Course]]) { + if period.count > 0 { + let periods = period.split(separator: " ") + periods.forEach { (period) in + let section = Int(String(period), radix: 16) ?? 0 + array[section - 1][day] = course + } + } + } + + private func initCourses() -> [[Domain.Course]] { + let array = [[Domain.Course]].init( + repeating: [Course].init( + repeating: Course(id: "", + name: "", + instructor: [], + periods: [], + classroom: []), + count: 7), + count: 13) + return array } } diff --git a/TAT/CurriculumViewController.swift b/TAT/CurriculumViewController.swift index cb4b4cb..e9b1d8b 100644 --- a/TAT/CurriculumViewController.swift +++ b/TAT/CurriculumViewController.swift @@ -20,7 +20,7 @@ final class CurriculumViewController: UIViewController { private lazy var activityIndicator: UIActivityIndicatorView = { let activityIndicator = UIActivityIndicatorView(style: .whiteLarge) - activityIndicator.color = .blue + activityIndicator.color = .systemPink return activityIndicator }() @@ -82,8 +82,10 @@ final class CurriculumViewController: UIViewController { } private func bindViewModel() { + let viewDidLoadTrigger = Observable.just(()) + let searchTrigger = Observable.merge(viewDidLoadTrigger, leftBarItem.rx.tap.asObservable()) let input = CurriculumViewModel.Input(targetStudentId: Observable.just("104440026"), - searchTrigger: leftBarItem.rx.tap.asObservable()) + searchTrigger: searchTrigger) let output = viewModel.transform(input: input) output.state @@ -99,9 +101,10 @@ final class CurriculumViewController: UIViewController { output.semesters .subscribe(onNext: { [weak self] (semesters) in - print(semesters) let semsterString = semesters.map { "\($0.year) 學年 第\($0.semester)學期" } self?.updateTitleView(by: semsterString) + }, onError: { (error) in + print(error) }) .disposed(by: rx.disposeBag) @@ -119,12 +122,12 @@ final class CurriculumViewController: UIViewController { } private func setUpLayouts() { - setUpActivityIndicator() setUpCollectionView() + setUpActivityIndicator() } private func setUpActivityIndicator() { - view.addSubview(activityIndicator) + collectionView.addSubview(activityIndicator) activityIndicator.snp.makeConstraints { (make) in make.center.equalToSuperview() } diff --git a/TAT/Login/LoginViewModel.swift b/TAT/Login/LoginViewModel.swift index cf77fac..9960098 100644 --- a/TAT/Login/LoginViewModel.swift +++ b/TAT/Login/LoginViewModel.swift @@ -52,6 +52,10 @@ extension LoginViewModel { .subscribe(onNext: { (_) in UserDefaults.standard.removeObject(forKey: "studentId") UserDefaults.standard.removeObject(forKey: "password") + UserDefaults.standard.removeObject(forKey: "semesters") + UserDefaults.standard.removeObject(forKey: "courses") + UserDefaults.standard.removeObject(forKey: "targetStudentId") + UserDefaults.standard.removeObject(forKey: "token") }) .disposed(by: rx.disposeBag) diff --git a/TAT/ViewModels/CourseViewModel.swift b/TAT/ViewModels/CourseViewModel.swift index e613e59..2047710 100644 --- a/TAT/ViewModels/CourseViewModel.swift +++ b/TAT/ViewModels/CourseViewModel.swift @@ -49,56 +49,45 @@ extension CourseViewModel { let inputData = Observable.combineLatest(input.year, input.semester, input.targetStudentId) - let state = PublishSubject() + let state = ReplaySubject.create(bufferSize: 1) + + state.subscribe(onNext: { (state) in + print(state) + }).disposed(by: rx.disposeBag) let courses = input.searchTrigger .withLatestFrom(inputData) - .flatMap { [unowned self] (year, semester, targetStudentId) -> Observable in + .filter { $0 != "" && $1 != "" && $2 != "" } + .flatMap { [unowned self] (year, semester, targetStudentId) -> Observable<[[Domain.Course]]> in state.onNext(.loading) - return self.curriculumsUseCase.courses(targetStudentId: targetStudentId, - year: year, - semester: semester) - } - - let coursesObseravle = courses - .asObservable() - .flatMap({ [unowned self] (curriculumCourses) -> Observable<[[Domain.Course]]> in - var array: [[Domain.Course]] = self.initCourses() - - for course in curriculumCourses.courses { - for (day, period) in course.periods.enumerated() { - self.reshapCourses(period: period, day: day, course: course, array: &array) - } - } + return self.generateCourses(year: year, semester: semester, targetStudentId: targetStudentId) + } + .share(replay: 1) + courses + .subscribe(onNext: { (courses) in state.onNext(.success) - return Observable.just(array) + if UserDefaults.standard.object(forKey: "courses") == nil { + guard let courses = try? JSONEncoder().encode(courses) else { return } + UserDefaults.standard.set(courses, forKey: "courses") + } + }, onError: { (error) in + print(error) + state.onNext(.error(message: "cannot get courses")) }) + .disposed(by: rx.disposeBag) - return Output(state: state, courses: coursesObseravle) + return Output(state: state, courses: courses) } - private func reshapCourses(period: String, day: Int, course: Course, array: inout[[Domain.Course]]) { - if period.count > 0 { - let periods = period.split(separator: " ") - periods.forEach { (period) in - let section = Int(String(period), radix: 16) ?? 0 - array[section - 1][day] = course - } + private func generateCourses(year: String, semester: String, targetStudentId: String) -> Observable<[[Domain.Course]]> { + guard let cachedData = UserDefaults.standard.object(forKey: "courses") as? Data, + let cachedCourses = try? JSONDecoder().decode([[Domain.Course]].self, from: cachedData) else { + return self.curriculumsUseCase.courses(targetStudentId: targetStudentId, + year: year, + semester: semester) } - } - - private func initCourses() -> [[Domain.Course]] { - let array = [[Domain.Course]].init( - repeating: [Course].init( - repeating: Course(id: "", - name: "", - instructor: [], - periods: [], - classroom: []), - count: 7), - count: 13) - return array + return Observable.just(cachedCourses) } } diff --git a/TAT/ViewModels/CurriculumViewModel.swift b/TAT/ViewModels/CurriculumViewModel.swift index ef4449e..a1337fc 100644 --- a/TAT/ViewModels/CurriculumViewModel.swift +++ b/TAT/ViewModels/CurriculumViewModel.swift @@ -42,18 +42,16 @@ extension CurriculumViewModel { func transform(input: Input) -> Output { let semesterInput = SemesterViewModel.Input(targetStudentId: input.targetStudentId) - let semesters = semesterViewModel.transform(input: semesterInput).semesters + let semesterOutput = semesterViewModel.transform(input: semesterInput) let courseInput = CourseViewModel.Input(year: Observable.just("108"), semester: Observable.just("1"), targetStudentId: input.targetStudentId, searchTrigger: input.searchTrigger) let courseOutput = courseViewModel.transform(input: courseInput) - let state = courseOutput.state - let courses = courseOutput.courses - return Output(state: state, - semesters: semesters, - courses: courses) + return Output(state: courseOutput.state, + semesters: semesterOutput.semesters, + courses: courseOutput.courses) } } diff --git a/TAT/ViewModels/SemesterViewModel.swift b/TAT/ViewModels/SemesterViewModel.swift index e8a139d..6ae70cf 100644 --- a/TAT/ViewModels/SemesterViewModel.swift +++ b/TAT/ViewModels/SemesterViewModel.swift @@ -36,12 +36,36 @@ final class SemesterViewModel: NSObject, ViewModelType { extension SemesterViewModel { func transform(input: SemesterViewModel.Input) -> SemesterViewModel.Output { + let semesters = input.targetStudentId + .filter { $0 != "" } + .flatMap { [unowned self] (targetStudentId) -> Observable<[Semester]> in + self.generateSemesters(from: targetStudentId) + } + .share() - let semesters = input.targetStudentId.flatMap { [unowned self] (targetStudentId) -> Observable<[Semester]> in - return self.curriculumsUseCase.semesters(targetStudentId: targetStudentId) - } + semesters + .subscribe(onNext: { (semesters) in + if UserDefaults.standard.object(forKey: "semesters") == nil { + guard let semesters = try? JSONEncoder().encode(semesters) else { return } + UserDefaults.standard.set(semesters, forKey: "semesters") + } + }, onError: { (error) in + print(error) + }) + .disposed(by: rx.disposeBag) return Output(semesters: semesters) } + private func generateSemesters(from targetStudentId: String) -> Observable<[Semester]> { + guard let cachedTargetStudentId = UserDefaults.standard.string(forKey: "targetStudentId"), + cachedTargetStudentId == targetStudentId else { + UserDefaults.standard.set(targetStudentId, forKey: "targetStudentId") + return curriculumsUseCase.semesters(targetStudentId: targetStudentId) + } + guard let cachedData = UserDefaults.standard.object(forKey: "semesters") as? Data, + let cachedSemesters = try? JSONDecoder().decode([Semester].self, from: cachedData) + else { fatalError("cannot cast to semesters") } + return Observable.just(cachedSemesters) + } }