Compare commits

..

No commits in common. "14c229175c1593b32ee655cca9b3294c8d6a516a" and "5946cd3ec0b378adac1e68ea454167261027b56b" have entirely different histories.

33 changed files with 306 additions and 884 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="CoreDataClassModel" representedClassName=".CoreDataClassModel" syncable="YES"> <entity name="ClassModel" representedClassName=".ClassModel" syncable="YES">
<attribute name="auditory" optional="YES" attributeType="String"/> <attribute name="auditory" optional="YES" attributeType="String"/>
<attribute name="comment" optional="YES" attributeType="String"/> <attribute name="comment" optional="YES" attributeType="String"/>
<attribute name="day" optional="YES" attributeType="Date" usesScalarValueType="NO"/> <attribute name="day" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
@ -12,11 +12,4 @@
<attribute name="starttime" optional="YES" attributeType="Date" usesScalarValueType="NO"/> <attribute name="starttime" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="subject" optional="YES" attributeType="String"/> <attribute name="subject" optional="YES" attributeType="String"/>
</entity> </entity>
<entity name="JsonClassModel" representedClassName="JsonClassModel" syncable="YES" codeGenerationType="class">
<attribute name="day" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="group" optional="YES" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="time" optional="YES" attributeType="String"/>
<attribute name="week" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
</model> </model>

View File

@ -8,7 +8,7 @@
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint"> BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent <BreakpointContent
uuid = "CF2C0E34-74B0-458B-AE66-E61DEB75A958" uuid = "CF2C0E34-74B0-458B-AE66-E61DEB75A958"
shouldBeEnabled = "Yes" shouldBeEnabled = "No"
ignoreCount = "0" ignoreCount = "0"
continueAfterRunningActions = "No" continueAfterRunningActions = "No"
filePath = "Schedule ICTIS/Main/Views/ProfessorAuditoryClassFieldView.swift" filePath = "Schedule ICTIS/Main/Views/ProfessorAuditoryClassFieldView.swift"

View File

@ -9,8 +9,8 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
@State private var selectedTab: Int = 1 @State private var selectedTab: Int = 1
@ObservedObject var vm: ScheduleViewModel @StateObject var vm = ScheduleViewModel()
@ObservedObject var networkMonitor: NetworkMonitor
var body: some View { var body: some View {
TabView(selection: $selectedTab) { TabView(selection: $selectedTab) {
Text("Tasks") Text("Tasks")
@ -20,7 +20,7 @@ struct ContentView: View {
} }
.tag(0) .tag(0)
MainView(vm: vm, networkMonitor: networkMonitor) MainView(vm: vm)
.tabItem { .tabItem {
Image(systemName: "house") Image(systemName: "house")
Text("Расписание") Text("Расписание")
@ -36,7 +36,24 @@ struct ContentView: View {
} }
.accentColor(Color("blueColor")) .accentColor(Color("blueColor"))
.onAppear { .onAppear {
let group1 = UserDefaults.standard.string(forKey: "group")
let group2 = UserDefaults.standard.string(forKey: "group2")
let group3 = UserDefaults.standard.string(forKey: "group3")
if let nameGroup1 = group1, nameGroup1 != "" {
vm.nameGroups.append(nameGroup1)
}
if let nameGroup2 = group2, nameGroup2 != "" {
vm.nameGroups.append(nameGroup2)
}
if let nameGroup3 = group3, nameGroup3 != "" {
vm.nameGroups.append(nameGroup3)
}
print("\(group1) - \(group2) - \(group3)")
vm.fetchWeekSchedule() vm.fetchWeekSchedule()
} }
} }
} }
#Preview {
ContentView()
}

View File

@ -1,3 +1,10 @@
//
// LoadingScheduleView.swift
// Schedule ICTIS
//
// Created by G412 on 19.02.2025.
//
import SwiftUI import SwiftUI
struct LoadingScheduleView: View { struct LoadingScheduleView: View {
@ -6,22 +13,7 @@ struct LoadingScheduleView: View {
ZStack { ZStack {
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 20) { VStack(spacing: 20) {
ForEach(0..<5, id: \.self) { _ in ForEach(0..<6, id: \.self) { _ in
VStack (alignment: .trailing) {
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(
gradient: Gradient(colors: [
isAnimated ? Color.gray.opacity(0.6) : Color.gray.opacity(0.3),
isAnimated ? Color.gray.opacity(0.3) : Color.gray.opacity(0.6)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 45, height: 20)
.padding(.horizontal, 20)
.animation(.linear(duration: 0.8).repeatForever(autoreverses: true), value: isAnimated)
RoundedRectangle(cornerRadius: 20) RoundedRectangle(cornerRadius: 20)
.fill( .fill(
LinearGradient( LinearGradient(
@ -38,11 +30,10 @@ struct LoadingScheduleView: View {
.animation(.linear(duration: 0.8).repeatForever(autoreverses: true), value: isAnimated) .animation(.linear(duration: 0.8).repeatForever(autoreverses: true), value: isAnimated)
} }
} }
}
.onAppear { .onAppear {
isAnimated.toggle() isAnimated.toggle()
} }
.padding(.top, 10) .padding(.top, 30)
} }
} }
} }

View File

@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
struct CreatedClassView: View { struct CreatedClassView: View {
@ObservedObject var _class: CoreDataClassModel @ObservedObject var _class: ClassModel
var provider = ClassProvider.shared var provider = ClassProvider.shared
var body: some View { var body: some View {
let existingCopy = try? provider.viewContext.existingObject(with: _class.objectID) let existingCopy = try? provider.viewContext.existingObject(with: _class.objectID)

View File

@ -1,50 +0,0 @@
//
// FilterGroupsView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 21.03.2025.
//
import SwiftUI
struct FilterGroupsView: View {
@ObservedObject var vm: ScheduleViewModel
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(vm.filteringGroups, id: \.self) { group in
VStack {
Text(group)
.foregroundColor(Color("customGray3"))
.font(.custom("Montserrat-Medium", fixedSize: 14))
.padding(.horizontal, 15)
.padding(.vertical, 7)
}
.background(Color.white)
.overlay (
Group {
if vm.showOnlyChoosenGroup == group {
RoundedRectangle(cornerRadius: 20)
.stroke(Color("blueColor"), lineWidth: 3)
}
}
)
.cornerRadius(20)
.onTapGesture {
vm.showOnlyChoosenGroup = group
}
}
}
.padding(.horizontal)
}
.onAppear {
vm.updateFilteringGroups()
}
.padding(.bottom, 10)
}
}
#Preview {
@Previewable @ObservedObject var vm = ScheduleViewModel()
FilterGroupsView(vm: vm)
}

View File

@ -11,9 +11,9 @@ struct MainView: View {
@State private var searchText: String = "" @State private var searchText: String = ""
@State private var isShowingMonthSlider: Bool = false @State private var isShowingMonthSlider: Bool = false
@ObservedObject var vm: ScheduleViewModel @ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
@FocusState private var isFocusedSearchBar: Bool @FocusState private var isFocusedSearchBar: Bool
@State private var isScrolling: Bool = false @State private var isScrolling: Bool = false
var body: some View { var body: some View {
VStack { VStack {
SearchBarView(text: $searchText, isFocused: _isFocusedSearchBar, vm: vm, isShowingMonthSlider: $isShowingMonthSlider) SearchBarView(text: $searchText, isFocused: _isFocusedSearchBar, vm: vm, isShowingMonthSlider: $isShowingMonthSlider)
@ -23,12 +23,11 @@ struct MainView: View {
} }
} }
CurrentDateView() CurrentDateView()
FilterGroupsView(vm: vm)
if vm.isLoading { if vm.isLoading {
LoadingScheduleView() LoadingScheduleView()
} }
else { else {
ScheduleView(vm: vm, networkMonitor: networkMonitor, isScrolling: $isScrolling) ScheduleView(vm: vm, isScrolling: $isScrolling)
} }
} }
.alert(isPresented: $vm.isShowingAlertForIncorrectGroup, error: vm.errorInNetwork) { error in .alert(isPresented: $vm.isShowingAlertForIncorrectGroup, error: vm.errorInNetwork) { error in
@ -83,15 +82,16 @@ struct MainView: View {
if (!isShowingMonthSlider) { if (!isShowingMonthSlider) {
WeekTabView(vm: vm) WeekTabView(vm: vm)
.transition(.opacity) .transition(.opacity)
.animation(.easeInOut(duration: 0.25), value: isShowingMonthSlider)
} }
else { else {
MonthTabView(vm: vm) MonthTabView(vm: vm)
.transition(.opacity) .transition(.opacity)
.animation(.linear(duration: 0.5), value: isShowingMonthSlider)
} }
} }
.padding(.horizontal) .padding(.horizontal)
.animation(.easeInOut(duration: 0.25), value: isShowingMonthSlider)
} }
} }
#Preview {
ContentView()
}

View File

@ -1,134 +1,76 @@
//
// ScheduleView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 05.12.2024.
// ктбо2-6
import SwiftUI import SwiftUI
import CoreData
struct ScheduleView: View { struct ScheduleView: View {
@ObservedObject var vm: ScheduleViewModel @ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor @FetchRequest(fetchRequest: ClassModel.all()) private var classes // Делаем запрос в CoreData и получаем список сохраненных пар
@FetchRequest(fetchRequest: CoreDataClassModel.all()) private var classes // Список пар добавленных пользователем @State private var selectedClass: ClassModel? = nil
@FetchRequest(fetchRequest: JsonClassModel.all()) private var subjects // Список пар сохраненных в CoreData
@State private var selectedClass: CoreDataClassModel? = nil
@State private var lastOffset: CGFloat = 0 @State private var lastOffset: CGFloat = 0
@State private var scrollTimer: Timer? = nil @State private var scrollTimer: Timer? = nil
@State private var isShowingMyPairs = false
@Binding var isScrolling: Bool @Binding var isScrolling: Bool
var provider = ClassProvider.shared var provider = ClassProvider.shared
var hasVPK: Bool {
private var hasSubjectsToShow: Bool { return vm.vpks.indices.contains(vm.selectedIndex) && vm.vpks[vm.selectedIndex].dropFirst().contains { !$0.isEmpty }
subjects.contains { subject in
subject.week == vm.week
} }
}
private var hasClassesToShow: Bool {
classes.contains { _class in
_class.day == vm.selectedDay
}
}
var body: some View { var body: some View {
ZStack(alignment: .top) { if vm.isLoading {
if networkMonitor.isConnected {
onlineContent
} else {
offlineContent
}
gradientOverlay
}
.onAppear {
deleteClassesFormCoreDataIfMonday()
if networkMonitor.isConnected {
checkSavingOncePerDay()
}
}
.sheet(item: $selectedClass, onDismiss: { selectedClass = nil }) { _class in
CreateEditClassView(vm: .init(provider: provider, _class: _class), day: vm.selectedDay)
}
}
// Онлайн-контент (с интернетом)
private var onlineContent: some View {
Group {
if vm.errorInNetwork == .timeout {
NetworkErrorView()
} else if vm.isLoading {
LoadingScheduleView() LoadingScheduleView()
} else if vm.errorInNetwork != .invalidResponse {
scheduleScrollView(isOnline: true)
} else {
NoScheduleView()
} }
} else {
.onAppear { if vm.errorInNetwork != .invalidResponse {
if vm.classesGroups.isEmpty { ZStack (alignment: .top) {
vm.fetchWeekSchedule()
}
}
}
// Оффлайн-контент (без интернета)
private var offlineContent: some View {
scheduleScrollView(isOnline: false)
}
// Общий ScrollView для расписания
private func scheduleScrollView(isOnline: Bool) -> some View {
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 30) { VStack (spacing: 30) {
subjectsSection(isOnline: isOnline) VStack (alignment: .leading, spacing: 10) {
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..<vm.classesGroups.count, id: \.self) { dayIndex in ForEach(0..<vm.classesGroups.count, id: \.self) { dayIndex in
if dayIndex == vm.selectedIndex { if dayIndex == vm.selectedIndex {
ForEach(vm.classesGroups[dayIndex]) { info in ForEach(vm.classesGroups[dayIndex]) { info in
if vm.showOnlyChoosenGroup == "Все" || info.group == vm.showOnlyChoosenGroup { VStack (alignment: .trailing) {
SubjectView(info: ClassInfo(subject: info.subject, group: info.group, time: info.time), vm: vm) 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)
} else { .cornerRadius(20)
let filteredSubjects = subjects.filter { $0.day == Int16(vm.selectedIndex) } .shadow(color: .black.opacity(0.25), radius: 4, x: 2, y: 2)
if (filteredSubjects.isEmpty || vm.week != 0) && !hasClassesToShow {
NetworkErrorView()
} else {
ForEach(filteredSubjects, id: \.self) { subject in
if (vm.showOnlyChoosenGroup == "Все" || subject.group == vm.showOnlyChoosenGroup) && vm.week == 0 {
SubjectView(info: ClassInfo(subject: subject.name!, group: subject.group!, time: subject.time!), vm: vm)
} }
} }
} }
} }
} }
}
// Секция "Мои пары"
private var myPairsSection: some View {
Group {
if classes.contains(where: { daysAreEqual($0.day, vm.selectedDay) }) { if classes.contains(where: { daysAreEqual($0.day, vm.selectedDay) }) {
VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 20) {
Text("Мои пары") Text("Мои пары")
@ -144,112 +86,50 @@ struct ScheduleView: View {
} }
} }
} }
.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
// Градиентный оверлей // Останавливаем предыдущий таймер
private var gradientOverlay: some View { scrollTimer?.invalidate()
// Запускаем новый таймер
scrollTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in
// Скролл остановился
isScrolling = false
}
}
lastOffset = offset
}
.onDisappear {
scrollTimer?.invalidate()
}
VStack { VStack {
LinearGradient( LinearGradient(gradient: Gradient(colors: [Color("background").opacity(0.95), Color.white.opacity(0.1)]), startPoint: .top, endPoint: .bottom)
gradient: Gradient(colors: [Color("background").opacity(0.95), Color.white.opacity(0.1)]),
startPoint: .top,
endPoint: .bottom
)
} }
.frame(width: UIScreen.main.bounds.width, height: 15) .frame(width: UIScreen.main.bounds.width, height: 15)
} }
} //Sheet будет открываться, когда selectedClass будет становиться не nil
.sheet(item: $selectedClass,
extension ScheduleView { onDismiss: {
private func deleteClassesFormCoreDataIfMonday() { selectedClass = nil
let today = Date() },
let calendar = Calendar.current content: { _class in
let weekday = calendar.component(.weekday, from: today) CreateEditClassView(vm: .init(provider: provider, _class: _class), day: vm.selectedDay)
})
if weekday == 6 {
for _class in classes {
if _class.day < today {
do {
try provider.delete(_class, in: provider.viewContext)
} catch {
print("❌ - Ошибка при удалении: \(error)")
} }
else {
NoScheduleView()
} }
} }
} }
}
func saveGroupsToMemory() {
var indexOfTheDay: Int16 = 0
let context = provider.newContext // Создаем новый контекст
context.perform {
for dayIndex in 0..<self.vm.classesGroups.count {
let classesForDay = self.vm.classesGroups[dayIndex]
// Проходим по всем занятиям текущего дня
for classInfo in classesForDay {
let newClass = JsonClassModel(context: context)
// Заполняем атрибуты
newClass.name = classInfo.subject
newClass.group = classInfo.group
newClass.time = classInfo.time
newClass.day = indexOfTheDay
newClass.week = Int16(vm.week)
}
indexOfTheDay += 1
}
// Сохраняем изменения в CoreData
do {
try self.provider.persist(in: context)
print("✅ Успешно сохранено в CoreData")
} catch {
print("❌ Ошибка при сохранении в CoreData: \(error)")
}
}
}
@MainActor
func deleteAllJsonClassModelsSync() throws {
let context = provider.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = 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")
}
}
} }
struct ViewOffsetKey: PreferenceKey { struct ViewOffsetKey: PreferenceKey {
@ -259,3 +139,7 @@ struct ViewOffsetKey: PreferenceKey {
value += nextValue() value += nextValue()
} }
} }
#Preview {
ContentView()
}

View File

@ -29,13 +29,9 @@ struct SearchBarView: View {
.onSubmit { .onSubmit {
self.isFocused = false self.isFocused = false
if (!text.isEmpty) { if (!text.isEmpty) {
vm.nameToHtml[vm.searchingGroup] = nil
vm.removeFromSchedule(group: vm.searchingGroup)
text = transformStringToFormat(text)
vm.searchingGroup = text vm.searchingGroup = text
vm.nameToHtml[text] = "" vm.updateArrayOfGroups()
vm.fetchWeekSchedule() vm.fetchWeekSchedule()
vm.updateFilteringGroups()
} }
self.text = "" self.text = ""
} }
@ -90,3 +86,6 @@ struct SearchBarView: View {
} }
} }
#Preview {
ContentView()
}

View File

@ -1,61 +0,0 @@
//
// 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])
}
}
}

View File

@ -15,31 +15,31 @@ struct MonthTabView: View {
@ObservedObject var vm: ScheduleViewModel @ObservedObject var vm: ScheduleViewModel
var body: some View { var body: some View {
VStack { VStack {
HStack (spacing: 33) { HStack (spacing: 34) {
ForEach(MockData.daysOfWeek.indices, id: \.self) { index in ForEach(MockData.daysOfWeek.indices, id: \.self) { index in
Text(MockData.daysOfWeek[index]) Text(MockData.daysOfWeek[index])
.font(.custom("Montserrat-SemiBold", fixedSize: 15)) .font(.custom("Montserrat-SemiBold", fixedSize: 15))
.foregroundColor(MockData.daysOfWeek[index] == "Вс" ? Color(.red) : Color("customGray2")) .foregroundColor(MockData.daysOfWeek[index] == "Вс" ? Color(.red) : Color("customGray2"))
.padding(.top, 13)
.foregroundColor(.gray)
} }
} }
.padding(.top, 14)
TabView(selection: $currentMonthIndex) { TabView(selection: $currentMonthIndex) {
ForEach(monthSlider.indices, id: \.self) { index in ForEach(monthSlider.indices, id: \.self) { index in
let month = monthSlider[index] let month = monthSlider[index]
MonthView(month) MonthView(month)
.tag(index) .tag(index)
.transition(.slide)
} }
} }
.padding(.top, -25)
.padding(.bottom, -10)
.padding(.horizontal, -15) .padding(.horizontal, -15)
.tabViewStyle(.page(indexDisplayMode: .never)) .tabViewStyle(.page(indexDisplayMode: .never))
//.animation(.easeIn(duration: 0.3), value: currentMonthIndex)
} }
.frame(height: 220) .onAppear(perform: {
.padding(.top, 26)
.padding(.bottom, 20)
.onAppear {
updateMonthScreenViewForNewGroup() updateMonthScreenViewForNewGroup()
} })
.onChange(of: currentMonthIndex, initial: false) { oldValue, newValue in .onChange(of: currentMonthIndex, initial: false) { oldValue, newValue in
if newValue == 0 || newValue == (monthSlider.count - 1) { if newValue == 0 || newValue == (monthSlider.count - 1) {
createMonth = true createMonth = true
@ -82,3 +82,6 @@ struct MonthTabView: View {
} }
#Preview {
ContentView()
}

View File

@ -44,3 +44,7 @@ struct WeekTabView: View {
} }
} }
} }
#Preview {
ContentView()
}

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
import CoreData import CoreData
final class CoreDataClassModel: NSManagedObject, Identifiable { final class ClassModel: NSManagedObject, Identifiable {
@NSManaged var auditory: String @NSManaged var auditory: String
@NSManaged var professor: String @NSManaged var professor: String
@NSManaged var subject: String @NSManaged var subject: String
@ -47,29 +47,29 @@ final class CoreDataClassModel: NSManagedObject, Identifiable {
} }
// Расширение для загрузки данных из памяти // Расширение для загрузки данных из памяти
extension CoreDataClassModel { extension ClassModel {
// Получаем все данные из памяти // Получаем все данные из памяти
private static var classesFetchRequest: NSFetchRequest<CoreDataClassModel> { private static var classesFetchRequest: NSFetchRequest<ClassModel> {
NSFetchRequest(entityName: "CoreDataClassModel") NSFetchRequest(entityName: "ClassModel")
} }
// Получаем все данные и сортируем их по дню // Получаем все данные и сортируем их по дню
// Этот метод будет использоваться на View(ScheduleView), где отображаются пары // Этот метод будет использоваться на View(ScheduleView), где отображаются пары
static func all() -> NSFetchRequest<CoreDataClassModel> { static func all() -> NSFetchRequest<ClassModel> {
let request: NSFetchRequest<CoreDataClassModel> = classesFetchRequest let request: NSFetchRequest<ClassModel> = classesFetchRequest
request.sortDescriptors = [ request.sortDescriptors = [
NSSortDescriptor(keyPath: \CoreDataClassModel.day, ascending: true) NSSortDescriptor(keyPath: \ClassModel.day, ascending: true)
] ]
return request return request
} }
} }
extension CoreDataClassModel { extension ClassModel {
@discardableResult @discardableResult
static func makePreview(count: Int, in context: NSManagedObjectContext) -> [CoreDataClassModel] { static func makePreview(count: Int, in context: NSManagedObjectContext) -> [ClassModel] {
var classes = [CoreDataClassModel]() var classes = [ClassModel]()
for i in 0..<count { for i in 0..<count {
let _class = CoreDataClassModel(context: context) let _class = ClassModel(context: context)
_class.subject = "Предмет \(i)" _class.subject = "Предмет \(i)"
_class.auditory = "Аудитория \(i)" _class.auditory = "Аудитория \(i)"
_class.professor = "Преподаватель \(i)" _class.professor = "Преподаватель \(i)"
@ -81,11 +81,11 @@ extension CoreDataClassModel {
return classes return classes
} }
static func preview(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel { static func preview(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> ClassModel {
return makePreview(count: 1, in: context)[0] return makePreview(count: 1, in: context)[0]
} }
static func empty(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel { static func empty(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> ClassModel {
return CoreDataClassModel(context: context) return ClassModel(context: context)
} }
} }

View File

@ -9,11 +9,11 @@ import Foundation
// MARK: - Welcome // MARK: - Welcome
struct Welcome: Decodable { struct Welcome: Decodable {
let choices: [Subject] let choices: [Choice]
} }
// MARK: - Choice // MARK: - Choice
struct Subject: Decodable, Identifiable { struct Choice: Decodable, Identifiable {
let name: String let name: String
let id: String let id: String
let group: String let group: String

View File

@ -1,46 +0,0 @@
//
// 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<JsonClassModel> {
NSFetchRequest(entityName: "JsonClassModel")
}
// Получаем все данные и сортируем их по дню
// Этот метод будет использоваться на View(ScheduleView), где отображаются пары
static func all() -> NSFetchRequest<JsonClassModel> {
let request: NSFetchRequest<JsonClassModel> = subjectsFetchRequest
request.sortDescriptors = [
NSSortDescriptor(keyPath: \JsonClassModel.time, ascending: true)
]
return request
}
}

View File

@ -1,31 +0,0 @@
//
// 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()
}

View File

@ -49,11 +49,11 @@ final class ClassProvider {
} }
} }
func exists(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) -> CoreDataClassModel? { func exists(_ lesson: ClassModel, in context: NSManagedObjectContext) -> ClassModel? {
try? context.existingObject(with: lesson.objectID) as? CoreDataClassModel try? context.existingObject(with: lesson.objectID) as? ClassModel
} }
func delete(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) throws { func delete(_ lesson: ClassModel, in context: NSManagedObjectContext) throws {
if let existingClass = exists(lesson, in: context) { if let existingClass = exists(lesson, in: context) {
context.delete(existingClass) context.delete(existingClass)
Task(priority: .background) { Task(priority: .background) {
@ -76,20 +76,3 @@ extension EnvironmentValues {
return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" 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()
}
}
}
}
}

View File

@ -9,16 +9,10 @@ import SwiftUI
@main @main
struct Schedule_ICTISApp: App { struct Schedule_ICTISApp: App {
@StateObject private var networkMonitor = NetworkMonitor()
@StateObject var vm = ScheduleViewModel()
var provider = ClassProvider.shared
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView(vm: vm, networkMonitor: networkMonitor) ContentView()
.environment(\.managedObjectContext, ClassProvider.shared.viewContext) .environment(\.managedObjectContext, ClassProvider.shared.viewContext)
.onAppear {
vm.fillDictForVm()
}
} }
} }
} }

View File

@ -25,8 +25,9 @@ struct FavGroupsView: View {
.cornerRadius(10) .cornerRadius(10)
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {
vm.removeFromSchedule(group: firstFavGroup)
UserDefaults.standard.set("", forKey: "group") UserDefaults.standard.set("", forKey: "group")
vm.updateArrayOfGroups()
vm.fetchWeekSchedule()
} label: { } label: {
Label("Удалить", systemImage: "trash") Label("Удалить", systemImage: "trash")
} }
@ -42,8 +43,9 @@ struct FavGroupsView: View {
.cornerRadius(10) .cornerRadius(10)
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {
vm.removeFromSchedule(group: secondFavGroup)
UserDefaults.standard.set("", forKey: "group2") UserDefaults.standard.set("", forKey: "group2")
vm.updateArrayOfGroups()
vm.fetchWeekSchedule()
} label: { } label: {
Label("Удалить", systemImage: "trash") Label("Удалить", systemImage: "trash")
} }
@ -59,8 +61,9 @@ struct FavGroupsView: View {
.cornerRadius(10) .cornerRadius(10)
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {
vm.removeFromSchedule(group: thirdFavGroup)
UserDefaults.standard.set("", forKey: "group3") UserDefaults.standard.set("", forKey: "group3")
vm.updateArrayOfGroups()
vm.fetchWeekSchedule()
} label: { } label: {
Label("Удалить", systemImage: "trash") Label("Удалить", systemImage: "trash")
} }

View File

@ -25,8 +25,9 @@ struct FavVPKView: View {
.cornerRadius(10) .cornerRadius(10)
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {
vm.removeFromSchedule(group: firstFavVPK)
UserDefaults.standard.set("", forKey: "vpk1") UserDefaults.standard.set("", forKey: "vpk1")
vm.updateArrayOfGroups()
vm.fetchWeekSchedule()
} label: { } label: {
Label("Удалить", systemImage: "trash") Label("Удалить", systemImage: "trash")
} }
@ -42,8 +43,9 @@ struct FavVPKView: View {
.cornerRadius(10) .cornerRadius(10)
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {
vm.removeFromSchedule(group: secondFavVPK)
UserDefaults.standard.set("", forKey: "vpk2") UserDefaults.standard.set("", forKey: "vpk2")
vm.updateArrayOfGroups()
vm.fetchWeekSchedule()
} label: { } label: {
Label("Удалить", systemImage: "trash") Label("Удалить", systemImage: "trash")
} }
@ -59,8 +61,9 @@ struct FavVPKView: View {
.cornerRadius(10) .cornerRadius(10)
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {
vm.removeFromSchedule(group: thirdFavVPK)
UserDefaults.standard.set("", forKey: "vpk3") UserDefaults.standard.set("", forKey: "vpk3")
vm.updateArrayOfGroups()
vm.fetchWeekSchedule()
} label: { } label: {
Label("Удалить", systemImage: "trash") Label("Удалить", systemImage: "trash")
} }

View File

@ -1,55 +0,0 @@
//
// ListOfGroupsView.swift
// Schedule ICTIS
//
// Created by G412 on 13.03.2025.
//
import SwiftUI
struct ListOfGroupsView: View {
@Environment(\.dismiss) private var dismiss
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var serchGroupsVM: SearchGroupsViewModel
var firstFavVPK: String
var secondFavVPK: String
var thirdFavVPK: String
var body: some View {
ScrollView(.vertical, showsIndicators: true) {
ForEach(serchGroupsVM.groups) { item in
if item.name.starts(with: "ВПК") || item.name.starts(with: "мВПК") {
VStack {
Rectangle()
.frame(height: 1)
.foregroundColor(Color("customGray1"))
.padding(.horizontal, 10)
HStack {
Text(item.name)
.foregroundColor(.black)
.font(.custom("Montserrat-SemiBold", fixedSize: 15))
Spacer()
}
.padding(.horizontal, 10)
.padding(.top, 2)
.padding(.bottom, 2)
.frame(width: UIScreen.main.bounds.width, height: 30)
.background(Color("background"))
.onTapGesture {
if firstFavVPK == "" {
UserDefaults.standard.set(item.name, forKey: "vpk1")
} else if secondFavVPK == "" {
UserDefaults.standard.set(item.name, forKey: "vpk2")
} else {
UserDefaults.standard.set(item.name, forKey: "vpk3")
}
vm.nameToHtml[item.name] = ""
vm.fetchWeekSchedule()
vm.updateFilteringGroups()
dismiss()
}
}
}
}
}
}
}

View File

@ -49,7 +49,6 @@ struct SelectingGroupView: View {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if vm.errorInNetwork == .noError { if vm.errorInNetwork == .noError {
vm.errorInNetwork = nil vm.errorInNetwork = nil
text = transformStringToFormat(text)
if firstFavGroup == "" { if firstFavGroup == "" {
UserDefaults.standard.set(text, forKey: "group") UserDefaults.standard.set(text, forKey: "group")
} else if secondFavGroup == "" { } else if secondFavGroup == "" {
@ -57,9 +56,8 @@ struct SelectingGroupView: View {
} else { } else {
UserDefaults.standard.set(text, forKey: "group3") UserDefaults.standard.set(text, forKey: "group3")
} }
vm.nameToHtml[text] = "" vm.updateArrayOfGroups()
vm.fetchWeekSchedule() vm.fetchWeekSchedule()
vm.updateFilteringGroups()
self.isLoading = false self.isLoading = false
self.text = "" self.text = ""
dismiss() dismiss()
@ -95,7 +93,7 @@ struct SelectingGroupView: View {
if isLoading { if isLoading {
LoadingView(isLoading: $isLoading) LoadingView(isLoading: $isLoading)
} }
//if isFocused { if isFocused {
ScrollView(.vertical, showsIndicators: true) { ScrollView(.vertical, showsIndicators: true) {
ForEach(serchGroupsVM.groups) { item in ForEach(serchGroupsVM.groups) { item in
if item.name.starts(with: "КТ") { //Отображаем только группы(без аудиторий и преподавателей) if item.name.starts(with: "КТ") { //Отображаем только группы(без аудиторий и преподавателей)
@ -118,15 +116,12 @@ struct SelectingGroupView: View {
.onTapGesture { .onTapGesture {
if firstFavGroup == "" { if firstFavGroup == "" {
UserDefaults.standard.set(item.name, forKey: "group") UserDefaults.standard.set(item.name, forKey: "group")
vm.nameToHtml[item.name] = ""
} else if secondFavGroup == "" { } else if secondFavGroup == "" {
UserDefaults.standard.set(item.name, forKey: "group2") UserDefaults.standard.set(item.name, forKey: "group2")
vm.nameToHtml[item.name] = ""
} else { } else {
UserDefaults.standard.set(item.name, forKey: "group3") UserDefaults.standard.set(item.name, forKey: "group3")
vm.nameToHtml[item.name] = ""
} }
vm.updateFilteringGroups() vm.updateArrayOfGroups()
vm.fetchWeekSchedule() vm.fetchWeekSchedule()
dismiss() dismiss()
} }
@ -134,7 +129,7 @@ struct SelectingGroupView: View {
} }
} }
} }
//} }
} }
.padding(.horizontal, 10) .padding(.horizontal, 10)
.background(Color("background")) .background(Color("background"))

View File

@ -56,9 +56,7 @@ struct SelectingVPKView: View {
} else { } else {
UserDefaults.standard.set(text, forKey: "vpk3") UserDefaults.standard.set(text, forKey: "vpk3")
} }
text = transformStringToFormat(text) vm.updateArrayOfGroups()
vm.nameToHtml[text] = ""
vm.updateFilteringGroups()
vm.fetchWeekSchedule() vm.fetchWeekSchedule()
self.isLoading = false self.isLoading = false
self.text = "" self.text = ""
@ -96,7 +94,43 @@ struct SelectingVPKView: View {
if isLoading { if isLoading {
LoadingView(isLoading: $isLoading) LoadingView(isLoading: $isLoading)
} }
ListOfGroupsView(vm: vm, serchGroupsVM: serchGroupsVM, firstFavVPK: firstFavVPK, secondFavVPK: secondFavVPK, thirdFavVPK: thirdFavVPK) if isFocused {
ScrollView(.vertical, showsIndicators: true) {
ForEach(serchGroupsVM.groups) { item in
if item.name.starts(with: "ВПК") {
VStack {
Rectangle()
.frame(height: 1)
.foregroundColor(Color("customGray1"))
.padding(.horizontal, 10)
HStack {
Text(item.name)
.foregroundColor(.black)
.font(.custom("Montserrat-SemiBold", fixedSize: 15))
Spacer()
}
.padding(.horizontal, 10)
.padding(.top, 2)
.padding(.bottom, 2)
.frame(width: UIScreen.main.bounds.width, height: 30)
.background(Color("background"))
.onTapGesture {
if firstFavVPK == "" {
UserDefaults.standard.set(item.name, forKey: "vpk1")
} else if secondFavVPK == "" {
UserDefaults.standard.set(item.name, forKey: "vpk2")
} else {
UserDefaults.standard.set(item.name, forKey: "vpk3")
}
vm.updateArrayOfGroups()
vm.fetchWeekSchedule()
dismiss()
}
}
}
}
}
}
} }
.padding(.horizontal, 10) .padding(.horizontal, 10)
.background(Color("background")) .background(Color("background"))

View File

@ -58,3 +58,7 @@ struct TabBarView: View {
} }
} }
} }
#Preview {
ContentView()
}

View File

@ -87,11 +87,9 @@ extension Date {
func createPreviousMonth() -> [MonthWeek] { func createPreviousMonth() -> [MonthWeek] {
let calendar = Calendar.current let calendar = Calendar.current
let startOfFirstDate = calendar.startOfDay(for: self) let startOfFirstDate = calendar.startOfDay(for: self)
guard let previousDate = calendar.date(byAdding: .weekOfMonth, value: -5, to: startOfFirstDate) else { guard let previousDate = calendar.date(byAdding: .month, value: -1, to: startOfFirstDate) else {
return [] return []
} }
print("Start of first date \(startOfFirstDate)")
print("Previous date \(previousDate)")
return fetchMonth(previousDate) return fetchMonth(previousDate)
} }

View File

@ -8,36 +8,6 @@
import SwiftUI import SwiftUI
extension View { 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 { func isSameDate(_ date1: Date, _ date2: Date) -> Bool {
return Calendar.current.isDate(date1, inSameDayAs: date2) return Calendar.current.isDate(date1, inSameDayAs: date2)
} }
@ -213,12 +183,11 @@ extension WeekTabView {
extension WeekViewForWeek { extension WeekViewForWeek {
func paginateWeek() { func paginateWeek() {
let calendar = Calendar.current let calendar = Calendar.current
let groupsKeys = Array (vm.nameToHtml.keys)
if weekSlider.indices.contains(currentWeekIndex) { if weekSlider.indices.contains(currentWeekIndex) {
if let firstDate = weekSlider[currentWeekIndex].first?.date, if let firstDate = weekSlider[currentWeekIndex].first?.date,
currentWeekIndex == 0 { currentWeekIndex == 0 {
vm.week -= 1 vm.week -= 1
if !groupsKeys.isEmpty { if !vm.nameGroups.isEmpty {
vm.fetchWeekSchedule(isOtherWeek: true) vm.fetchWeekSchedule(isOtherWeek: true)
} }
if UserDefaults.standard.string(forKey: "vpk") != nil { if UserDefaults.standard.string(forKey: "vpk") != nil {
@ -234,7 +203,7 @@ extension WeekViewForWeek {
if let lastDate = weekSlider[currentWeekIndex].last?.date, if let lastDate = weekSlider[currentWeekIndex].last?.date,
currentWeekIndex == (weekSlider.count - 1) { currentWeekIndex == (weekSlider.count - 1) {
vm.week += 1 vm.week += 1
if !groupsKeys.isEmpty { if !vm.nameGroups.isEmpty {
vm.fetchWeekSchedule(isOtherWeek: true) vm.fetchWeekSchedule(isOtherWeek: true)
} }
weekSlider.append(lastDate.createNextWeek()) weekSlider.append(lastDate.createNextWeek())
@ -274,14 +243,13 @@ extension WeekViewForMonth {
print("На одной неделе") print("На одной неделе")
} }
else { else {
let groupsKeys = Array(vm.nameToHtml.keys)
var difBetweenWeeks = weeksBetween(startDate: vm.selectedDay, endDate: day.date) var difBetweenWeeks = weeksBetween(startDate: vm.selectedDay, endDate: day.date)
if day.date < vm.selectedDay { if day.date < vm.selectedDay {
difBetweenWeeks = difBetweenWeeks * -1 difBetweenWeeks = difBetweenWeeks * -1
} }
print(difBetweenWeeks) print(difBetweenWeeks)
vm.week += difBetweenWeeks vm.week += difBetweenWeeks
if !groupsKeys.isEmpty { if !vm.nameGroups.isEmpty {
vm.fetchWeekSchedule(isOtherWeek: true) vm.fetchWeekSchedule(isOtherWeek: true)
} }
if UserDefaults.standard.string(forKey: "vpk") != nil { if UserDefaults.standard.string(forKey: "vpk") != nil {
@ -300,24 +268,19 @@ extension MonthTabView {
let currentMonth = Date().fetchMonth(vm.selectedDay) let currentMonth = Date().fetchMonth(vm.selectedDay)
if let firstDate = currentMonth.first?.week[0].date { if let firstDate = currentMonth.first?.week[0].date {
let temp = firstDate.createPreviousMonth() monthSlider.append(firstDate.createPreviousMonth())
print("First date - \(firstDate)")
print(temp)
monthSlider.append(temp)
} }
monthSlider.append(currentMonth) monthSlider.append(currentMonth)
if let lastDate = currentMonth.last?.week[6].date { if let lastDate = currentMonth.last?.week[6].date {
let temp = lastDate.createNextMonth() monthSlider.append(lastDate.createNextMonth())
monthSlider.append(temp)
} }
} }
} }
func paginateMonth(_ indexOfWeek: Int = 0) { func paginateMonth(_ indexOfWeek: Int = 0) {
let calendar = Calendar.current let calendar = Calendar.current
let groupsKeys = Array (vm.nameToHtml.keys)
if monthSlider.indices.contains(currentMonthIndex) { if monthSlider.indices.contains(currentMonthIndex) {
if let firstDate = monthSlider[currentMonthIndex].first?.week[0].date, if let firstDate = monthSlider[currentMonthIndex].first?.week[0].date,
currentMonthIndex == 0 { currentMonthIndex == 0 {
@ -327,7 +290,7 @@ extension MonthTabView {
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -5, to: vm.selectedDay) ?? Date.init() vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -5, to: vm.selectedDay) ?? Date.init()
vm.updateSelectedDayIndex() vm.updateSelectedDayIndex()
vm.week -= 5 vm.week -= 5
if !groupsKeys.isEmpty { if !vm.nameGroups.isEmpty {
vm.fetchWeekSchedule(isOtherWeek: true) vm.fetchWeekSchedule(isOtherWeek: true)
} }
} }
@ -340,7 +303,7 @@ extension MonthTabView {
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 5, to: vm.selectedDay) ?? Date.init() vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 5, to: vm.selectedDay) ?? Date.init()
vm.updateSelectedDayIndex() vm.updateSelectedDayIndex()
vm.week += 5 vm.week += 5
if !groupsKeys.isEmpty { if !vm.nameGroups.isEmpty {
vm.fetchWeekSchedule(isOtherWeek: true) vm.fetchWeekSchedule(isOtherWeek: true)
} }
} }

View File

@ -11,8 +11,8 @@ enum NetworkError: String, Error, LocalizedError {
case invalidUrl case invalidUrl
case invalidResponse case invalidResponse
case invalidData case invalidData
case noNetwork
case noError case noError
case timeout
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
@ -22,8 +22,8 @@ enum NetworkError: String, Error, LocalizedError {
"InvalidResponse" "InvalidResponse"
case .invalidData: case .invalidData:
"Проверьте номер группы" "Проверьте номер группы"
case .timeout: case .noNetwork:
"Ошибка сети" "No network connection"
case .noError: case .noError:
"Нет ошибки" "Нет ошибки"
} }
@ -37,8 +37,8 @@ enum NetworkError: String, Error, LocalizedError {
"Для этой недели расписания еще нет" "Для этой недели расписания еще нет"
case .invalidData: case .invalidData:
"Похоже такой группы не существует" "Похоже такой группы не существует"
case .timeout: case .noNetwork:
"Проверьте соединение с интернетом" "Проверьте подключение к интернету и попробуйте заново"
case .noError: case .noError:
"Ошибки нет" "Ошибки нет"
} }

View File

@ -14,14 +14,9 @@ final class NetworkManager {
private let decoder = JSONDecoder() private let decoder = JSONDecoder()
private let urlForGroup = "https://webictis.sfedu.ru/schedule-api/?query=" private let urlForGroup = "https://webictis.sfedu.ru/schedule-api/?query="
private let urlForWeek = "https://webictis.sfedu.ru/schedule-api/?group=" private let urlForWeek = "https://webictis.sfedu.ru/schedule-api/?group="
private let customSession: URLSession // Кастомная сессия для ограничения времени ответа от сервера
//MARK: Initializer //MARK: Initializer
private init() { private init() {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 3 // Таймаут запроса 10 секунд
configuration.timeoutIntervalForResource = 3 // Таймаут ресурса 15 секунд
self.customSession = URLSession(configuration: configuration)
decoder.dateDecodingStrategy = .iso8601 decoder.dateDecodingStrategy = .iso8601
} }
@ -37,7 +32,7 @@ final class NetworkManager {
func getSchedule(_ group: String) async throws -> Schedule { func getSchedule(_ group: String) async throws -> Schedule {
let newUrlForGroup = makeUrlForGroup(group) let newUrlForGroup = makeUrlForGroup(group)
guard let url = URL(string: newUrlForGroup) else { throw NetworkError.invalidUrl } guard let url = URL(string: newUrlForGroup) else { throw NetworkError.invalidUrl }
let (data, response) = try await customSession.data(from: url) let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse } guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse }
do { do {
@ -52,7 +47,7 @@ final class NetworkManager {
let newUrlForWeek = makeUrlForWeek(numOfWeek, htmlNameOfGroup) let newUrlForWeek = makeUrlForWeek(numOfWeek, htmlNameOfGroup)
print(newUrlForWeek) print(newUrlForWeek)
guard let url = URL(string: newUrlForWeek) else { throw NetworkError.invalidUrl } guard let url = URL(string: newUrlForWeek) else { throw NetworkError.invalidUrl }
let (data, response) = try await customSession.data(from: url) let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse } guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse }
do { do {
@ -66,7 +61,7 @@ final class NetworkManager {
func getGroups(group: String) async throws -> Welcome { func getGroups(group: String) async throws -> Welcome {
let newUrlForGroups = makeUrlForGroup(group) let newUrlForGroups = makeUrlForGroup(group)
guard let url = URL(string: newUrlForGroups) else { throw NetworkError.invalidUrl } guard let url = URL(string: newUrlForGroups) else { throw NetworkError.invalidUrl }
let (data, response) = try await customSession.data(from: url) let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse } guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse }
do { do {

View File

@ -1,37 +0,0 @@
//
// 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()
}
}

View File

@ -2,14 +2,14 @@
// EditClassViewModel.swift // EditClassViewModel.swift
// Schedule ICTIS // Schedule ICTIS
// //
// Created by Egor Mironov on 18.12.2024. // Created by G412 on 18.12.2024.
// //
import Foundation import Foundation
import CoreData import CoreData
final class EditClassViewModel: ObservableObject { final class EditClassViewModel: ObservableObject {
@Published var _class: CoreDataClassModel @Published var _class: ClassModel
let isNew: Bool let isNew: Bool
@ -17,7 +17,7 @@ final class EditClassViewModel: ObservableObject {
private let context: NSManagedObjectContext private let context: NSManagedObjectContext
init(provider: ClassProvider, _class: CoreDataClassModel? = nil) { init(provider: ClassProvider, _class: ClassModel? = nil) {
self.provider = provider self.provider = provider
self.context = provider.newContext self.context = provider.newContext
@ -27,7 +27,7 @@ final class EditClassViewModel: ObservableObject {
self.isNew = false self.isNew = false
} }
else { else {
self._class = CoreDataClassModel(context: self.context) self._class = ClassModel(context: self.context)
self.isNew = true self.isNew = true
} }
} }

View File

@ -1,35 +0,0 @@
//
// 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)
}
}

View File

@ -11,11 +11,10 @@ import SwiftUICore
@MainActor @MainActor
final class ScheduleViewModel: ObservableObject { final class ScheduleViewModel: ObservableObject {
//MARK: Properties //MARK: Properties
@Published var nameToHtml: [String : String] = [:] @Published var nameGroups: [String] = []
@Published var numbersNTMLGroups: [String] = []
@Published var classesGroups: [[ClassInfo]] = [] @Published var classesGroups: [[ClassInfo]] = []
@Published var searchingGroup = "" @Published var searchingGroup = ""
@Published var filteringGroups: [String] = ["Все"]
@Published var showOnlyChoosenGroup: String = "Все"
//Schedule //Schedule
@Published var weekScheduleGroup: Table = Table( @Published var weekScheduleGroup: Table = Table(
@ -27,7 +26,7 @@ final class ScheduleViewModel: ObservableObject {
link: "" link: ""
) )
@Published var selectedDay: Date = .init() @Published var selectedDay: Date = .init()
@Published var selectedIndex: Int = 0 @Published var selectedIndex: Int = 1
@Published var week: Int = 0 @Published var week: Int = 0
@Published var isFirstStartOffApp = true @Published var isFirstStartOffApp = true
@ -45,8 +44,7 @@ final class ScheduleViewModel: ObservableObject {
// Если другая неделя, запрашиваем расписание по неделе и номеру группу(в HTML формате) // Если другая неделя, запрашиваем расписание по неделе и номеру группу(в HTML формате)
if isOtherWeek { if isOtherWeek {
let groupHTMLs = Array(self.nameToHtml.values) for groupHTML in numbersNTMLGroups {
for groupHTML in groupHTMLs {
let schedule = try await NetworkManager.shared.getScheduleForOtherWeek(self.week, groupHTML) let schedule = try await NetworkManager.shared.getScheduleForOtherWeek(self.week, groupHTML)
let table = schedule.table.table let table = schedule.table.table
let nameOfGroup = schedule.table.name let nameOfGroup = schedule.table.name
@ -63,11 +61,10 @@ final class ScheduleViewModel: ObservableObject {
} }
} }
} else { } else {
let groupNames = Array(self.nameToHtml.keys) for groupName in nameGroups {
for groupName in groupNames {
let schedule = try await NetworkManager.shared.getSchedule(groupName) let schedule = try await NetworkManager.shared.getSchedule(groupName)
let numberHTML = schedule.table.group let numberHTML = schedule.table.group
self.nameToHtml[groupName] = numberHTML self.numbersNTMLGroups.append(numberHTML)
let table = schedule.table.table let table = schedule.table.table
self.week = schedule.table.week self.week = schedule.table.week
@ -94,10 +91,7 @@ final class ScheduleViewModel: ObservableObject {
// Сортируем по времени // Сортируем по времени
self.sortClassesByTime() self.sortClassesByTime()
} catch { } catch {
if let urlError = error as? URLError, urlError.code == .timedOut { if let error = error as? NetworkError {
errorInNetwork = .timeout
print("Ошибка: превышено время ожидания ответа от сервера")
} else if let error = error as? NetworkError {
switch error { switch error {
case .invalidResponse: case .invalidResponse:
errorInNetwork = .invalidResponse errorInNetwork = .invalidResponse
@ -107,9 +101,9 @@ final class ScheduleViewModel: ObservableObject {
default: default:
print("Неизвестная ошибка: \(error)") print("Неизвестная ошибка: \(error)")
} }
isLoading = false
print("Есть ошибка: \(error)") print("Есть ошибка: \(error)")
} }
isLoading = false
} }
} }
} }
@ -162,29 +156,9 @@ final class ScheduleViewModel: ObservableObject {
} }
} }
func removeFromSchedule(group: String) { func updateArrayOfGroups() {
self.nameToHtml[group] = nil self.nameGroups.removeAll()
self.numbersNTMLGroups.removeAll()
for i in classesGroups.indices {
// Сначала находим индексы элементов для удаления
let indicesToRemove = classesGroups[i].indices.filter { j in
classesGroups[i][j].group.lowercased() == group.lowercased()
}
// Удаляем элементы в обратном порядке, чтобы индексы оставались корректными
for j in indicesToRemove.reversed() {
classesGroups[i].remove(at: j)
}
}
}
func updateFilteringGroups() {
self.filteringGroups = ["Все"]
let keys = self.nameToHtml.keys
self.filteringGroups.append(contentsOf: keys)
}
func fillDictForVm() {
let group1 = UserDefaults.standard.string(forKey: "group") let group1 = UserDefaults.standard.string(forKey: "group")
let group2 = UserDefaults.standard.string(forKey: "group2") let group2 = UserDefaults.standard.string(forKey: "group2")
let group3 = UserDefaults.standard.string(forKey: "group3") let group3 = UserDefaults.standard.string(forKey: "group3")
@ -192,22 +166,23 @@ final class ScheduleViewModel: ObservableObject {
let vpk2 = UserDefaults.standard.string(forKey: "vpk2") let vpk2 = UserDefaults.standard.string(forKey: "vpk2")
let vpk3 = UserDefaults.standard.string(forKey: "vpk3") let vpk3 = UserDefaults.standard.string(forKey: "vpk3")
if let nameGroup1 = group1, nameGroup1 != "" { if let nameGroup1 = group1, nameGroup1 != "" {
nameToHtml[nameGroup1] = "" self.nameGroups.append(nameGroup1)
} }
if let nameGroup2 = group2, nameGroup2 != "" { if let nameGroup2 = group2, nameGroup2 != "" {
nameToHtml[nameGroup2] = "" self.nameGroups.append(nameGroup2)
} }
if let nameGroup3 = group3, nameGroup3 != "" { if let nameGroup3 = group3, nameGroup3 != "" {
nameToHtml[nameGroup3] = "" self.nameGroups.append(nameGroup3)
} }
if let nameVpk1 = vpk1, nameVpk1 != "" { if let nameVPK1 = vpk1, nameVPK1 != "" {
nameToHtml[nameVpk1] = "" self.nameGroups.append(nameVPK1)
} }
if let nameVpk2 = vpk2, nameVpk2 != "" { if let nameVPK2 = vpk2, nameVPK2 != "" {
nameToHtml[nameVpk2] = "" self.nameGroups.append(nameVPK2)
} }
if let nameVpk3 = vpk3, nameVpk3 != "" { if let nameVPK3 = vpk3, nameVPK3 != "" {
nameToHtml[nameVpk3] = "" self.nameGroups.append(nameVPK3)
} }
self.nameGroups.append(self.searchingGroup)
} }
} }

View File

@ -9,7 +9,7 @@ import Foundation
@MainActor @MainActor
final class SearchGroupsViewModel: ObservableObject { final class SearchGroupsViewModel: ObservableObject {
@Published var groups: [Subject] = [] @Published var groups: [Choice] = []
func fetchGroups(group: String) { func fetchGroups(group: String) {
Task { Task {
@ -17,11 +17,7 @@ final class SearchGroupsViewModel: ObservableObject {
var groups: Welcome var groups: Welcome
groups = try await NetworkManager.shared.getGroups(group: group) groups = try await NetworkManager.shared.getGroups(group: group)
self.groups = groups.choices self.groups = groups.choices
if (group == "кт") {
self.sortGroups()
} else {
self.sortVPK()
}
} }
catch { catch {
if let error = error as? NetworkError { if let error = error as? NetworkError {
@ -37,101 +33,4 @@ final class SearchGroupsViewModel: ObservableObject {
} }
} }
} }
// Метод сортировки
func sortGroups() {
groups.sort { (group1, group2) in
// Извлекаем компоненты из названия групп
let components1 = extractComponents(from: group1.name)
let components2 = extractComponents(from: group2.name)
// Сравниваем сначала по номеру курса (первая цифра после букв)
if components1.courseNumber != components2.courseNumber {
return components1.courseNumber < components2.courseNumber
}
// Если номера курсов равны, сравниваем по номеру группы (число после дефиса)
return components1.groupNumber < components2.groupNumber
}
}
// Вспомогательная структура для хранения извлеченных компонентов
private struct GroupComponents {
let courseNumber: Int
let groupNumber: Int
}
// Метод для извлечения числовых компонентов из названия группы
private func extractComponents(from name: String) -> GroupComponents {
// Находим индекс дефиса
guard let hyphenIndex = name.firstIndex(of: "-") else {
return GroupComponents(courseNumber: 0, groupNumber: 0)
}
// Извлекаем часть до дефиса (буквы и номер курса)
let prefix = String(name[..<hyphenIndex])
// Извлекаем часть после дефиса (номер группы)
let suffix = String(name[name.index(after: hyphenIndex)...])
// Извлекаем цифры из prefix
let courseNumberString = prefix.trimmingCharacters(in: CharacterSet.letters)
let courseNumber = Int(courseNumberString) ?? 0
// Преобразуем suffix в число
let groupNumber = Int(suffix) ?? 0
return GroupComponents(courseNumber: courseNumber, groupNumber: groupNumber)
}
// Метод сортировки для ВПК групп
func sortVPK() {
groups.sort { (group1, group2) in
let components1 = extractVPKComponents(from: group1.name)
let components2 = extractVPKComponents(from: group2.name)
// Сравниваем по типу (мВПК после ВПК)
if components1.isMinor != components2.isMinor {
return !components1.isMinor // ВПК идет перед мВПК
}
// Сравниваем по первому номеру
if components1.firstNumber != components2.firstNumber {
return components1.firstNumber < components2.firstNumber
}
// Если первые номера равны, сравниваем по второму номеру (если есть)
// Используем nil coalescing для случаев, когда второго номера нет
return (components1.secondNumber ?? Int.max) < (components2.secondNumber ?? Int.max)
}
}
// Вспомогательная структура для хранения компонентов ВПК
private struct VPKComponents {
let isMinor: Bool // true для мВПК, false для ВПК
let firstNumber: Int
let secondNumber: Int?
}
// Метод для извлечения компонентов из названия ВПК группы
private func extractVPKComponents(from name: String) -> VPKComponents {
let isMinor = name.hasPrefix("мВПК")
// Убираем префикс и разбиваем по дефису
let cleanName = isMinor ? name.replacingOccurrences(of: "мВПК-", with: "") : name.replacingOccurrences(of: "ВПК ", with: "")
let components = cleanName.split(separator: "-")
// Извлекаем первый номер
let firstNumberString = String(components[0]).trimmingCharacters(in: .whitespaces)
let firstNumber = Int(firstNumberString) ?? 0
// Извлекаем второй номер, если он есть
let secondNumber: Int?
if components.count > 1 {
secondNumber = Int(components[1]) ?? 0
} else {
secondNumber = nil
}
return VPKComponents(isMinor: isMinor, firstNumber: firstNumber, secondNumber: secondNumber)
}
} }