From 14c229175c1593b32ee655cca9b3294c8d6a516a Mon Sep 17 00:00:00 2001 From: Vladimir Dubovik Date: Thu, 3 Apr 2025 11:10:21 +0300 Subject: [PATCH] Commit --- .../ClassDataModel.xcdatamodel/contents | 9 +- .../xcdebugger/Breakpoints_v2.xcbkptlist | 2 +- Schedule ICTIS/ContentView.swift | 38 +- .../Main/Views/CreatedClassView.swift | 2 +- .../Main/Views/FilterGroupsView.swift | 50 +++ Schedule ICTIS/Main/Views/MainView.swift | 11 +- Schedule ICTIS/Main/Views/ScheduleView.swift | 357 ++++++++++++------ Schedule ICTIS/Main/Views/SearchBarView.swift | 5 +- Schedule ICTIS/Main/Views/SubjectView.swift | 61 +++ .../Main/Views/TabViews/MonthTabView.swift | 19 +- .../Main/Views/TabViews/WeekTabView.swift | 4 - ...ssModel.swift => CoreDataClassModel.swift} | 28 +- Schedule ICTIS/Model/GroupsModel.swift | 4 +- Schedule ICTIS/Model/JsonClassModel.swift | 46 +++ Schedule ICTIS/NetworkErrorView.swift | 31 ++ Schedule ICTIS/Provider/ClassProvider.swift | 23 +- Schedule ICTIS/Schedule_ICTISApp.swift | 8 +- .../Settings/ListOfGroupsView.swift | 1 + .../Settings/SelectingGroupView.swift | 7 +- .../Settings/SelectingVPKView.swift | 2 + Schedule ICTIS/TabBar/TabBarView.swift | 4 - .../Extensions/Date+Extensions.swift | 4 +- .../Extensions/View+Extensions.swift | 38 +- .../Utilities/Network/NetworkError.swift | 10 +- .../Utilities/Network/NetworkManager.swift | 11 +- .../Utilities/Network/NetworkMonitor.swift | 37 ++ .../ViewModel/EditClassViewModel.swift | 8 +- .../ViewModel/SaveScheduleViewModel.swift | 35 ++ .../ViewModel/ScheduleViewModel.swift | 44 ++- .../ViewModel/SearchGroupsViewModel.swift | 2 +- 30 files changed, 674 insertions(+), 227 deletions(-) create mode 100644 Schedule ICTIS/Main/Views/FilterGroupsView.swift create mode 100644 Schedule ICTIS/Main/Views/SubjectView.swift rename Schedule ICTIS/Model/{ClassModel.swift => CoreDataClassModel.swift} (79%) create mode 100644 Schedule ICTIS/Model/JsonClassModel.swift create mode 100644 Schedule ICTIS/NetworkErrorView.swift create mode 100644 Schedule ICTIS/Utilities/Network/NetworkMonitor.swift create mode 100644 Schedule ICTIS/ViewModel/SaveScheduleViewModel.swift diff --git a/ClassDataModel.xcdatamodeld/ClassDataModel.xcdatamodel/contents b/ClassDataModel.xcdatamodeld/ClassDataModel.xcdatamodel/contents index 65ba196..efc5761 100644 --- a/ClassDataModel.xcdatamodeld/ClassDataModel.xcdatamodel/contents +++ b/ClassDataModel.xcdatamodeld/ClassDataModel.xcdatamodel/contents @@ -1,6 +1,6 @@ - + @@ -12,4 +12,11 @@ + + + + + + + \ No newline at end of file diff --git a/Schedule ICTIS.xcodeproj/xcuserdata/g412.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Schedule ICTIS.xcodeproj/xcuserdata/g412.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 1c2910a..e96d03a 100644 --- a/Schedule ICTIS.xcodeproj/xcuserdata/g412.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/Schedule ICTIS.xcodeproj/xcuserdata/g412.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -8,7 +8,7 @@ BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint"> some View { + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 30) { + subjectsSection(isOnline: isOnline) + myPairsSection + } + .frame(width: UIScreen.main.bounds.width) + .padding(.bottom, 100) + .padding(.top, 10) + .background(GeometryReader { geometry in + Color.clear.preference(key: ViewOffsetKey.self, value: geometry.frame(in: .global).minY) + }) + } + .onPreferenceChange(ViewOffsetKey.self) { offset in + if offset != lastOffset { + isScrolling = true + scrollTimer?.invalidate() + scrollTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in + isScrolling = false + } + } + lastOffset = offset + } + .onDisappear { + scrollTimer?.invalidate() + } + } + + // Секция с парами + private func subjectsSection(isOnline: Bool) -> some View { + VStack(alignment: .leading, spacing: 10) { + if isOnline { + ForEach(0.. = JsonClassModel.fetchRequest() + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + + try context.execute(deleteRequest) + try context.save() + print("✅ Все объекты JsonClassModel успешно удалены") + } + + func checkSavingOncePerDay() { + let today = Date() + let calendar = Calendar.current + let todayStart = calendar.startOfDay(for: today) // Начало текущего дня + + // Получаем дату последнего выполнения из UserDefaults + let lastCheckDate = UserDefaults.standard.object(forKey: "LastSaving") as? Date ?? .distantPast + let lastCheckStart = calendar.startOfDay(for: lastCheckDate) + + print("Дата последнего сохранения расписания в CoreData: \(lastCheckDate)") + + // Проверяем, был ли уже выполнен код сегодня + if lastCheckStart < todayStart && networkMonitor.isConnected { + print("✅ Интернет есть, сохранение пар в CoreData") + vm.fillDictForVm() + vm.fetchWeekSchedule() + do { + try deleteAllJsonClassModelsSync() + } catch { + print("Ошибка при удалении: \(error)") + return + } + saveGroupsToMemory() + + // Сохраняем текущую дату как дату последнего выполнения + UserDefaults.standard.set(today, forKey: "LastSaving") + } } } @@ -136,7 +259,3 @@ struct ViewOffsetKey: PreferenceKey { value += nextValue() } } - -#Preview { - ContentView() -} diff --git a/Schedule ICTIS/Main/Views/SearchBarView.swift b/Schedule ICTIS/Main/Views/SearchBarView.swift index 199ced0..c061f1b 100644 --- a/Schedule ICTIS/Main/Views/SearchBarView.swift +++ b/Schedule ICTIS/Main/Views/SearchBarView.swift @@ -31,9 +31,11 @@ struct SearchBarView: View { if (!text.isEmpty) { vm.nameToHtml[vm.searchingGroup] = nil vm.removeFromSchedule(group: vm.searchingGroup) + text = transformStringToFormat(text) vm.searchingGroup = text vm.nameToHtml[text] = "" vm.fetchWeekSchedule() + vm.updateFilteringGroups() } self.text = "" } @@ -88,6 +90,3 @@ struct SearchBarView: View { } } -#Preview { - ContentView() -} diff --git a/Schedule ICTIS/Main/Views/SubjectView.swift b/Schedule ICTIS/Main/Views/SubjectView.swift new file mode 100644 index 0000000..4030bc0 --- /dev/null +++ b/Schedule ICTIS/Main/Views/SubjectView.swift @@ -0,0 +1,61 @@ +// +// SubjectView.swift +// Schedule ICTIS +// +// Created by Egor Mironov on 02.04.2025. +// + +import SwiftUI + +struct SubjectView: View { + let info: ClassInfo + @ObservedObject var vm: ScheduleViewModel + @State private var onlyOneGroup: Bool = false + + var body: some View { + VStack(alignment: .trailing) { + if !onlyOneGroup { + Text(info.group) + .font(.custom("Montserrat-Regular", fixedSize: 11)) + .foregroundColor(Color("grayForNameGroup")) + } + HStack(spacing: 15) { + VStack { + Text(convertTimeString(info.time)[0]) + .font(.custom("Montserrat-Regular", fixedSize: 15)) + .padding(.bottom, 1) + Text(convertTimeString(info.time)[1]) + .font(.custom("Montserrat-Regular", fixedSize: 15)) + .padding(.top, 1) + } + .frame(width: 48) + .padding(.top, 7) + .padding(.bottom, 7) + .padding(.leading, 10) + Rectangle() + .frame(width: 2) + .frame(maxHeight: UIScreen.main.bounds.height - 18) + .padding(.top, 7) + .padding(.bottom, 7) + .foregroundColor(getColorForClass(info.subject)) + Text(info.subject) + .font(.custom("Montserrat-Medium", fixedSize: 16)) + .lineSpacing(3) + .padding(.top, 9) + .padding(.bottom, 9) + Spacer() + } + .frame(maxWidth: UIScreen.main.bounds.width - 40, maxHeight: 230) + .background(Color.white) + .cornerRadius(20) + .shadow(color: .black.opacity(0.25), radius: 4, x: 2, y: 2) + } + .onAppear { + onlyOneGroup = (vm.showOnlyChoosenGroup != vm.filteringGroups[0]) + } + .padding(.bottom, onlyOneGroup ? 17 : 0) + .onChange(of: vm.showOnlyChoosenGroup) { oldValue, newValue in + onlyOneGroup = (newValue != vm.filteringGroups[0]) + } + } +} diff --git a/Schedule ICTIS/Main/Views/TabViews/MonthTabView.swift b/Schedule ICTIS/Main/Views/TabViews/MonthTabView.swift index 7c8514c..1f92190 100644 --- a/Schedule ICTIS/Main/Views/TabViews/MonthTabView.swift +++ b/Schedule ICTIS/Main/Views/TabViews/MonthTabView.swift @@ -15,31 +15,31 @@ struct MonthTabView: View { @ObservedObject var vm: ScheduleViewModel var body: some View { VStack { - HStack (spacing: 34) { + HStack (spacing: 33) { ForEach(MockData.daysOfWeek.indices, id: \.self) { index in Text(MockData.daysOfWeek[index]) .font(.custom("Montserrat-SemiBold", fixedSize: 15)) .foregroundColor(MockData.daysOfWeek[index] == "Вс" ? Color(.red) : Color("customGray2")) - .padding(.top, 13) - .foregroundColor(.gray) } } - .padding(.top, 14) TabView(selection: $currentMonthIndex) { ForEach(monthSlider.indices, id: \.self) { index in let month = monthSlider[index] MonthView(month) .tag(index) + .transition(.slide) } } - .padding(.top, -25) - .padding(.bottom, -10) .padding(.horizontal, -15) .tabViewStyle(.page(indexDisplayMode: .never)) + //.animation(.easeIn(duration: 0.3), value: currentMonthIndex) } - .onAppear(perform: { + .frame(height: 220) + .padding(.top, 26) + .padding(.bottom, 20) + .onAppear { updateMonthScreenViewForNewGroup() - }) + } .onChange(of: currentMonthIndex, initial: false) { oldValue, newValue in if newValue == 0 || newValue == (monthSlider.count - 1) { createMonth = true @@ -82,6 +82,3 @@ struct MonthTabView: View { } -#Preview { - ContentView() -} diff --git a/Schedule ICTIS/Main/Views/TabViews/WeekTabView.swift b/Schedule ICTIS/Main/Views/TabViews/WeekTabView.swift index da58428..30a896a 100644 --- a/Schedule ICTIS/Main/Views/TabViews/WeekTabView.swift +++ b/Schedule ICTIS/Main/Views/TabViews/WeekTabView.swift @@ -44,7 +44,3 @@ struct WeekTabView: View { } } } - -#Preview { - ContentView() -} diff --git a/Schedule ICTIS/Model/ClassModel.swift b/Schedule ICTIS/Model/CoreDataClassModel.swift similarity index 79% rename from Schedule ICTIS/Model/ClassModel.swift rename to Schedule ICTIS/Model/CoreDataClassModel.swift index b18c8d5..71f904f 100644 --- a/Schedule ICTIS/Model/ClassModel.swift +++ b/Schedule ICTIS/Model/CoreDataClassModel.swift @@ -8,7 +8,7 @@ import Foundation import CoreData -final class ClassModel: NSManagedObject, Identifiable { +final class CoreDataClassModel: NSManagedObject, Identifiable { @NSManaged var auditory: String @NSManaged var professor: String @NSManaged var subject: String @@ -47,29 +47,29 @@ final class ClassModel: NSManagedObject, Identifiable { } // Расширение для загрузки данных из памяти -extension ClassModel { +extension CoreDataClassModel { // Получаем все данные из памяти - private static var classesFetchRequest: NSFetchRequest { - NSFetchRequest(entityName: "ClassModel") + private static var classesFetchRequest: NSFetchRequest { + NSFetchRequest(entityName: "CoreDataClassModel") } // Получаем все данные и сортируем их по дню // Этот метод будет использоваться на View(ScheduleView), где отображаются пары - static func all() -> NSFetchRequest { - let request: NSFetchRequest = classesFetchRequest + static func all() -> NSFetchRequest { + let request: NSFetchRequest = classesFetchRequest request.sortDescriptors = [ - NSSortDescriptor(keyPath: \ClassModel.day, ascending: true) + NSSortDescriptor(keyPath: \CoreDataClassModel.day, ascending: true) ] return request } } -extension ClassModel { +extension CoreDataClassModel { @discardableResult - static func makePreview(count: Int, in context: NSManagedObjectContext) -> [ClassModel] { - var classes = [ClassModel]() + static func makePreview(count: Int, in context: NSManagedObjectContext) -> [CoreDataClassModel] { + var classes = [CoreDataClassModel]() for i in 0.. ClassModel { + static func preview(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel { return makePreview(count: 1, in: context)[0] } - static func empty(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> ClassModel { - return ClassModel(context: context) + static func empty(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel { + return CoreDataClassModel(context: context) } } diff --git a/Schedule ICTIS/Model/GroupsModel.swift b/Schedule ICTIS/Model/GroupsModel.swift index afc0a94..09b32ef 100644 --- a/Schedule ICTIS/Model/GroupsModel.swift +++ b/Schedule ICTIS/Model/GroupsModel.swift @@ -9,11 +9,11 @@ import Foundation // MARK: - Welcome struct Welcome: Decodable { - let choices: [Choice] + let choices: [Subject] } // MARK: - Choice -struct Choice: Decodable, Identifiable { +struct Subject: Decodable, Identifiable { let name: String let id: String let group: String diff --git a/Schedule ICTIS/Model/JsonClassModel.swift b/Schedule ICTIS/Model/JsonClassModel.swift new file mode 100644 index 0000000..a29168f --- /dev/null +++ b/Schedule ICTIS/Model/JsonClassModel.swift @@ -0,0 +1,46 @@ +// +// JsonClassModel.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 27.03.2025. +// + +import Foundation +import CoreData + +final class JsonDataClassModel: NSManagedObject, Identifiable { + @NSManaged var name: String + @NSManaged var group: String + @NSManaged var time: String + @NSManaged var day: Int16 + + // Здесь мы выполняем дополнительную инициализацию, назначая значения по умолчанию + // Этот метод вызывается всякий раз, когда объект Core Data вставляется в контекст + override func awakeFromInsert() { + super.awakeFromInsert() + + setPrimitiveValue("", forKey: "name") + setPrimitiveValue("", forKey: "group") + setPrimitiveValue("", forKey: "time") + setPrimitiveValue(0, forKey: "day") + setPrimitiveValue(0, forKey: "week") + } +} + +// Расширение для загрузки данных из памяти +extension JsonClassModel { + // Получаем все данные из памяти + private static var subjectsFetchRequest: NSFetchRequest { + NSFetchRequest(entityName: "JsonClassModel") + } + + // Получаем все данные и сортируем их по дню + // Этот метод будет использоваться на View(ScheduleView), где отображаются пары + static func all() -> NSFetchRequest { + let request: NSFetchRequest = subjectsFetchRequest + request.sortDescriptors = [ + NSSortDescriptor(keyPath: \JsonClassModel.time, ascending: true) + ] + return request + } +} diff --git a/Schedule ICTIS/NetworkErrorView.swift b/Schedule ICTIS/NetworkErrorView.swift new file mode 100644 index 0000000..a45f8ea --- /dev/null +++ b/Schedule ICTIS/NetworkErrorView.swift @@ -0,0 +1,31 @@ +// +// NetworkErrorView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 26.03.2025. +// + +import SwiftUI + +struct NetworkErrorView: View { + var body: some View { + VStack { + Spacer() + VStack { + Image(systemName: "wifi.slash") + .font(.system(size: 60, weight: .light)) + .frame(width: 70, height: 70) + Text("Восстановите подключение к интернету чтобы мы могли загрузить расписание") + .font(.custom("Montserrat-Medium", fixedSize: 15)) + .padding(.top, 5) + } + .padding(.horizontal, 30) + .padding(.top, UIScreen.main.bounds.height/8) + Spacer() + } + } +} + +#Preview { + NetworkErrorView() +} diff --git a/Schedule ICTIS/Provider/ClassProvider.swift b/Schedule ICTIS/Provider/ClassProvider.swift index 5e7f4a2..839a08f 100644 --- a/Schedule ICTIS/Provider/ClassProvider.swift +++ b/Schedule ICTIS/Provider/ClassProvider.swift @@ -49,11 +49,11 @@ final class ClassProvider { } } - func exists(_ lesson: ClassModel, in context: NSManagedObjectContext) -> ClassModel? { - try? context.existingObject(with: lesson.objectID) as? ClassModel + func exists(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) -> CoreDataClassModel? { + try? context.existingObject(with: lesson.objectID) as? CoreDataClassModel } - func delete(_ lesson: ClassModel, in context: NSManagedObjectContext) throws { + func delete(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) throws { if let existingClass = exists(lesson, in: context) { context.delete(existingClass) Task(priority: .background) { @@ -76,3 +76,20 @@ extension EnvironmentValues { return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" } } + +extension ClassProvider { + func exists(_ jsonClass: JsonClassModel, in context: NSManagedObjectContext) -> JsonClassModel? { + try? context.existingObject(with: jsonClass.objectID) as? JsonClassModel + } + + func delete(_ jsonClass: JsonClassModel, in context: NSManagedObjectContext) throws { + if let existingJsonClass = exists(jsonClass, in: context) { + context.delete(existingJsonClass) + Task(priority: .background) { + try await context.perform { + try context.save() + } + } + } + } +} diff --git a/Schedule ICTIS/Schedule_ICTISApp.swift b/Schedule ICTIS/Schedule_ICTISApp.swift index 3c3000d..9630afc 100644 --- a/Schedule ICTIS/Schedule_ICTISApp.swift +++ b/Schedule ICTIS/Schedule_ICTISApp.swift @@ -9,10 +9,16 @@ import SwiftUI @main struct Schedule_ICTISApp: App { + @StateObject private var networkMonitor = NetworkMonitor() + @StateObject var vm = ScheduleViewModel() + var provider = ClassProvider.shared var body: some Scene { WindowGroup { - ContentView() + ContentView(vm: vm, networkMonitor: networkMonitor) .environment(\.managedObjectContext, ClassProvider.shared.viewContext) + .onAppear { + vm.fillDictForVm() + } } } } diff --git a/Schedule ICTIS/Settings/ListOfGroupsView.swift b/Schedule ICTIS/Settings/ListOfGroupsView.swift index d6ae7aa..debdc28 100644 --- a/Schedule ICTIS/Settings/ListOfGroupsView.swift +++ b/Schedule ICTIS/Settings/ListOfGroupsView.swift @@ -44,6 +44,7 @@ struct ListOfGroupsView: View { } vm.nameToHtml[item.name] = "" vm.fetchWeekSchedule() + vm.updateFilteringGroups() dismiss() } } diff --git a/Schedule ICTIS/Settings/SelectingGroupView.swift b/Schedule ICTIS/Settings/SelectingGroupView.swift index 9064525..55b0308 100644 --- a/Schedule ICTIS/Settings/SelectingGroupView.swift +++ b/Schedule ICTIS/Settings/SelectingGroupView.swift @@ -49,17 +49,17 @@ struct SelectingGroupView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if vm.errorInNetwork == .noError { vm.errorInNetwork = nil + text = transformStringToFormat(text) if firstFavGroup == "" { UserDefaults.standard.set(text, forKey: "group") - vm.nameToHtml[text] = "" } else if secondFavGroup == "" { UserDefaults.standard.set(text, forKey: "group2") - vm.nameToHtml[text] = "" } else { UserDefaults.standard.set(text, forKey: "group3") - vm.nameToHtml[text] = "" } + vm.nameToHtml[text] = "" vm.fetchWeekSchedule() + vm.updateFilteringGroups() self.isLoading = false self.text = "" dismiss() @@ -126,6 +126,7 @@ struct SelectingGroupView: View { UserDefaults.standard.set(item.name, forKey: "group3") vm.nameToHtml[item.name] = "" } + vm.updateFilteringGroups() vm.fetchWeekSchedule() dismiss() } diff --git a/Schedule ICTIS/Settings/SelectingVPKView.swift b/Schedule ICTIS/Settings/SelectingVPKView.swift index 18fbbc2..7834406 100644 --- a/Schedule ICTIS/Settings/SelectingVPKView.swift +++ b/Schedule ICTIS/Settings/SelectingVPKView.swift @@ -56,7 +56,9 @@ struct SelectingVPKView: View { } else { UserDefaults.standard.set(text, forKey: "vpk3") } + text = transformStringToFormat(text) vm.nameToHtml[text] = "" + vm.updateFilteringGroups() vm.fetchWeekSchedule() self.isLoading = false self.text = "" diff --git a/Schedule ICTIS/TabBar/TabBarView.swift b/Schedule ICTIS/TabBar/TabBarView.swift index f583201..c47a758 100644 --- a/Schedule ICTIS/TabBar/TabBarView.swift +++ b/Schedule ICTIS/TabBar/TabBarView.swift @@ -58,7 +58,3 @@ struct TabBarView: View { } } } - -#Preview { - ContentView() -} diff --git a/Schedule ICTIS/Utilities/Extensions/Date+Extensions.swift b/Schedule ICTIS/Utilities/Extensions/Date+Extensions.swift index 135fd62..824689a 100644 --- a/Schedule ICTIS/Utilities/Extensions/Date+Extensions.swift +++ b/Schedule ICTIS/Utilities/Extensions/Date+Extensions.swift @@ -87,9 +87,11 @@ extension Date { func createPreviousMonth() -> [MonthWeek] { let calendar = Calendar.current let startOfFirstDate = calendar.startOfDay(for: self) - guard let previousDate = calendar.date(byAdding: .month, value: -1, to: startOfFirstDate) else { + guard let previousDate = calendar.date(byAdding: .weekOfMonth, value: -5, to: startOfFirstDate) else { return [] } + print("Start of first date \(startOfFirstDate)") + print("Previous date \(previousDate)") return fetchMonth(previousDate) } diff --git a/Schedule ICTIS/Utilities/Extensions/View+Extensions.swift b/Schedule ICTIS/Utilities/Extensions/View+Extensions.swift index 13d64de..441bc86 100644 --- a/Schedule ICTIS/Utilities/Extensions/View+Extensions.swift +++ b/Schedule ICTIS/Utilities/Extensions/View+Extensions.swift @@ -8,6 +8,36 @@ import SwiftUI extension View { + + func transformStringToFormat(_ input: String) -> String { + var result = input + + // Условие 1: начинается с "кт" + if result.lowercased().hasPrefix("кт") { + result = result.lowercased() + let firstTwo = String(result.prefix(2)).uppercased() + let rest = String(result.dropFirst(2)) + result = firstTwo + rest + return result + } + + // Условие 2: содержит "впк" + if result.lowercased().contains("впк") { + result = result.lowercased() + result = result.replacingOccurrences(of: "впк", with: "ВПК") + return result + } + + // Условие 3: содержит "мвпк" + if result.lowercased().contains("мвпк") { + result = result.lowercased() + result = result.replacingOccurrences(of: "впк", with: "ВПК") + return result + } + + return result + } + func isSameDate(_ date1: Date, _ date2: Date) -> Bool { return Calendar.current.isDate(date1, inSameDayAs: date2) } @@ -270,13 +300,17 @@ extension MonthTabView { let currentMonth = Date().fetchMonth(vm.selectedDay) if let firstDate = currentMonth.first?.week[0].date { - monthSlider.append(firstDate.createPreviousMonth()) + let temp = firstDate.createPreviousMonth() + print("First date - \(firstDate)") + print(temp) + monthSlider.append(temp) } monthSlider.append(currentMonth) if let lastDate = currentMonth.last?.week[6].date { - monthSlider.append(lastDate.createNextMonth()) + let temp = lastDate.createNextMonth() + monthSlider.append(temp) } } } diff --git a/Schedule ICTIS/Utilities/Network/NetworkError.swift b/Schedule ICTIS/Utilities/Network/NetworkError.swift index 804dad0..bfaba72 100644 --- a/Schedule ICTIS/Utilities/Network/NetworkError.swift +++ b/Schedule ICTIS/Utilities/Network/NetworkError.swift @@ -11,8 +11,8 @@ enum NetworkError: String, Error, LocalizedError { case invalidUrl case invalidResponse case invalidData - case noNetwork case noError + case timeout var errorDescription: String? { switch self { @@ -22,8 +22,8 @@ enum NetworkError: String, Error, LocalizedError { "InvalidResponse" case .invalidData: "Проверьте номер группы" - case .noNetwork: - "No network connection" + case .timeout: + "Ошибка сети" case .noError: "Нет ошибки" } @@ -37,8 +37,8 @@ enum NetworkError: String, Error, LocalizedError { "Для этой недели расписания еще нет" case .invalidData: "Похоже такой группы не существует" - case .noNetwork: - "Проверьте подключение к интернету и попробуйте заново" + case .timeout: + "Проверьте соединение с интернетом" case .noError: "Ошибки нет" } diff --git a/Schedule ICTIS/Utilities/Network/NetworkManager.swift b/Schedule ICTIS/Utilities/Network/NetworkManager.swift index cdb69c1..ff4ad9c 100644 --- a/Schedule ICTIS/Utilities/Network/NetworkManager.swift +++ b/Schedule ICTIS/Utilities/Network/NetworkManager.swift @@ -14,9 +14,14 @@ final class NetworkManager { private let decoder = JSONDecoder() private let urlForGroup = "https://webictis.sfedu.ru/schedule-api/?query=" private let urlForWeek = "https://webictis.sfedu.ru/schedule-api/?group=" + private let customSession: URLSession // Кастомная сессия для ограничения времени ответа от сервера //MARK: Initializer private init() { + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = 3 // Таймаут запроса 10 секунд + configuration.timeoutIntervalForResource = 3 // Таймаут ресурса 15 секунд + self.customSession = URLSession(configuration: configuration) decoder.dateDecodingStrategy = .iso8601 } @@ -32,7 +37,7 @@ final class NetworkManager { func getSchedule(_ group: String) async throws -> Schedule { let newUrlForGroup = makeUrlForGroup(group) guard let url = URL(string: newUrlForGroup) else { throw NetworkError.invalidUrl } - let (data, response) = try await URLSession.shared.data(from: url) + let (data, response) = try await customSession.data(from: url) guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse } do { @@ -47,7 +52,7 @@ final class NetworkManager { let newUrlForWeek = makeUrlForWeek(numOfWeek, htmlNameOfGroup) print(newUrlForWeek) guard let url = URL(string: newUrlForWeek) else { throw NetworkError.invalidUrl } - let (data, response) = try await URLSession.shared.data(from: url) + let (data, response) = try await customSession.data(from: url) guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse } do { @@ -61,7 +66,7 @@ final class NetworkManager { func getGroups(group: String) async throws -> Welcome { let newUrlForGroups = makeUrlForGroup(group) guard let url = URL(string: newUrlForGroups) else { throw NetworkError.invalidUrl } - let (data, response) = try await URLSession.shared.data(from: url) + let (data, response) = try await customSession.data(from: url) guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse } do { diff --git a/Schedule ICTIS/Utilities/Network/NetworkMonitor.swift b/Schedule ICTIS/Utilities/Network/NetworkMonitor.swift new file mode 100644 index 0000000..2ca2ba3 --- /dev/null +++ b/Schedule ICTIS/Utilities/Network/NetworkMonitor.swift @@ -0,0 +1,37 @@ +// +// NetworkMonitor.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 27.03.2025. +// + +import Network +import SwiftUI + +class NetworkMonitor: ObservableObject { + @Published var isConnected: Bool = false + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "NetworkMonitorQueue") + + init() { + startMonitoring() + } + + func startMonitoring() { + monitor.pathUpdateHandler = { [weak self] path in + DispatchQueue.main.async { + self?.isConnected = path.status == .satisfied + print(self?.isConnected == true ? "✅ Интернет подключен!" : "❌ Нет подключения к интернету.") + } + } + monitor.start(queue: queue) + } + + func stopMonitoring() { + monitor.cancel() + } + + deinit { + stopMonitoring() + } +} diff --git a/Schedule ICTIS/ViewModel/EditClassViewModel.swift b/Schedule ICTIS/ViewModel/EditClassViewModel.swift index 1706c1f..0b28d46 100644 --- a/Schedule ICTIS/ViewModel/EditClassViewModel.swift +++ b/Schedule ICTIS/ViewModel/EditClassViewModel.swift @@ -2,14 +2,14 @@ // EditClassViewModel.swift // Schedule ICTIS // -// Created by G412 on 18.12.2024. +// Created by Egor Mironov on 18.12.2024. // import Foundation import CoreData final class EditClassViewModel: ObservableObject { - @Published var _class: ClassModel + @Published var _class: CoreDataClassModel let isNew: Bool @@ -17,7 +17,7 @@ final class EditClassViewModel: ObservableObject { private let context: NSManagedObjectContext - init(provider: ClassProvider, _class: ClassModel? = nil) { + init(provider: ClassProvider, _class: CoreDataClassModel? = nil) { self.provider = provider self.context = provider.newContext @@ -27,7 +27,7 @@ final class EditClassViewModel: ObservableObject { self.isNew = false } else { - self._class = ClassModel(context: self.context) + self._class = CoreDataClassModel(context: self.context) self.isNew = true } } diff --git a/Schedule ICTIS/ViewModel/SaveScheduleViewModel.swift b/Schedule ICTIS/ViewModel/SaveScheduleViewModel.swift new file mode 100644 index 0000000..50d1dd6 --- /dev/null +++ b/Schedule ICTIS/ViewModel/SaveScheduleViewModel.swift @@ -0,0 +1,35 @@ +// +// SaveScheduleViewModel.swift +// Schedule ICTIS +// +// Created by Egor Mironov on 02.04.2025. +// + +import Foundation +import CoreData + +final class SaveScheduleViewModel: ObservableObject { + @Published var subject: JsonClassModel + + + private let provider: ClassProvider + + private let context: NSManagedObjectContext + + init(provider: ClassProvider, subject: JsonClassModel? = nil) { + self.provider = provider + self.context = provider.newContext + + if let subject, + let existingClassCopy = provider.exists(subject, in: context) { + self.subject = existingClassCopy + } + else { + self.subject = JsonClassModel(context: self.context) + } + } + + func save() throws { + try provider.persist(in: context) + } +} diff --git a/Schedule ICTIS/ViewModel/ScheduleViewModel.swift b/Schedule ICTIS/ViewModel/ScheduleViewModel.swift index 277ae01..58f5190 100644 --- a/Schedule ICTIS/ViewModel/ScheduleViewModel.swift +++ b/Schedule ICTIS/ViewModel/ScheduleViewModel.swift @@ -14,6 +14,8 @@ final class ScheduleViewModel: ObservableObject { @Published var nameToHtml: [String : String] = [:] @Published var classesGroups: [[ClassInfo]] = [] @Published var searchingGroup = "" + @Published var filteringGroups: [String] = ["Все"] + @Published var showOnlyChoosenGroup: String = "Все" //Schedule @Published var weekScheduleGroup: Table = Table( @@ -25,7 +27,7 @@ final class ScheduleViewModel: ObservableObject { link: "" ) @Published var selectedDay: Date = .init() - @Published var selectedIndex: Int = 1 + @Published var selectedIndex: Int = 0 @Published var week: Int = 0 @Published var isFirstStartOffApp = true @@ -92,7 +94,10 @@ final class ScheduleViewModel: ObservableObject { // Сортируем по времени self.sortClassesByTime() } catch { - if let error = error as? NetworkError { + if let urlError = error as? URLError, urlError.code == .timedOut { + errorInNetwork = .timeout + print("Ошибка: превышено время ожидания ответа от сервера") + } else if let error = error as? NetworkError { switch error { case .invalidResponse: errorInNetwork = .invalidResponse @@ -102,9 +107,9 @@ final class ScheduleViewModel: ObservableObject { default: print("Неизвестная ошибка: \(error)") } - isLoading = false print("Есть ошибка: \(error)") } + isLoading = false } } } @@ -172,4 +177,37 @@ final class ScheduleViewModel: ObservableObject { } } } + + func updateFilteringGroups() { + self.filteringGroups = ["Все"] + let keys = self.nameToHtml.keys + self.filteringGroups.append(contentsOf: keys) + } + + func fillDictForVm() { + let group1 = UserDefaults.standard.string(forKey: "group") + let group2 = UserDefaults.standard.string(forKey: "group2") + let group3 = UserDefaults.standard.string(forKey: "group3") + let vpk1 = UserDefaults.standard.string(forKey: "vpk1") + let vpk2 = UserDefaults.standard.string(forKey: "vpk2") + let vpk3 = UserDefaults.standard.string(forKey: "vpk3") + if let nameGroup1 = group1, nameGroup1 != "" { + nameToHtml[nameGroup1] = "" + } + if let nameGroup2 = group2, nameGroup2 != "" { + nameToHtml[nameGroup2] = "" + } + if let nameGroup3 = group3, nameGroup3 != "" { + nameToHtml[nameGroup3] = "" + } + if let nameVpk1 = vpk1, nameVpk1 != "" { + nameToHtml[nameVpk1] = "" + } + if let nameVpk2 = vpk2, nameVpk2 != "" { + nameToHtml[nameVpk2] = "" + } + if let nameVpk3 = vpk3, nameVpk3 != "" { + nameToHtml[nameVpk3] = "" + } + } } diff --git a/Schedule ICTIS/ViewModel/SearchGroupsViewModel.swift b/Schedule ICTIS/ViewModel/SearchGroupsViewModel.swift index 16b3e53..b5669e0 100644 --- a/Schedule ICTIS/ViewModel/SearchGroupsViewModel.swift +++ b/Schedule ICTIS/ViewModel/SearchGroupsViewModel.swift @@ -9,7 +9,7 @@ import Foundation @MainActor final class SearchGroupsViewModel: ObservableObject { - @Published var groups: [Choice] = [] + @Published var groups: [Subject] = [] func fetchGroups(group: String) { Task {