Compare commits

..

19 Commits

Author SHA1 Message Date
edfe97c6dc Commit 2025-04-04 11:01:33 +03:00
14c229175c Commit 2025-04-03 11:10:21 +03:00
99f2bd8a74 Commit 2025-03-14 12:47:18 +03:00
8bc7425e2a Commit 2025-03-13 12:26:41 +03:00
5946cd3ec0 One more commit 2025-03-13 11:13:36 +03:00
9bfd85ec3d Commit 2025-03-13 10:44:28 +03:00
15fbe5895c Commit 2025-03-13 09:24:50 +03:00
13de6fa302 Commit 2025-02-27 13:07:19 +03:00
b719ab300d Commit 2025-02-25 15:25:45 +03:00
9c6515a2f5 Commit 2025-02-21 14:07:01 +03:00
bb268cc6ad Commit 2025-02-19 12:43:52 +03:00
9f717d83df Commit 2025-02-06 13:28:57 +03:00
06416138d9 Now fonts are correct 2025-01-29 19:04:37 +03:00
4ee81cf2ea Done with createClassView 2025-01-28 12:54:52 +03:00
4a295b9b88 Alomost done with CreateClassView 2025-01-23 15:44:35 +03:00
4c3a46d40e Fixed bug with new number of group on not current week 2025-01-22 18:15:55 +03:00
3eb5fb73eb Done with CoreData working. Saving, updating and deleting are working correct now 2025-01-21 14:54:24 +03:00
b4704bd4fc Done with CoreData 2025-01-21 12:11:02 +03:00
e6b217aba4 Commit 2024-12-24 14:54:02 +03:00
92 changed files with 3076 additions and 807 deletions

View File

@ -1,6 +1,6 @@
<?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="">
<entity name="ClassModel" representedClassName=".ClassModel" syncable="YES">
<entity name="CoreDataClassModel" representedClassName=".CoreDataClassModel" syncable="YES">
<attribute name="auditory" optional="YES" attributeType="String"/>
<attribute name="comment" optional="YES" attributeType="String"/>
<attribute name="day" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
@ -12,4 +12,11 @@
<attribute name="starttime" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="subject" optional="YES" attributeType="String"/>
</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>

View File

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

View File

@ -0,0 +1,32 @@
//
// LoadingView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 11.12.2024.
//
import SwiftUI
struct ConnectingToNetworkView: View {
@State private var isAnimating = false
var body: some View {
VStack {
Text("Ожидание сети")
.font(.custom("Montserrat-Medium", fixedSize: 18))
Circle()
.trim(from: 0.2, to: 1.0)
.stroke(Color("blueColor"), lineWidth: 3)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isAnimating ? 360 : 0))
.animation(
Animation.linear(duration: 0.6).repeatForever(autoreverses: false),
value: isAnimating
)
.onAppear { isAnimating = true }
}
}
}
#Preview {
ConnectingToNetworkView()
}

View File

@ -8,36 +8,70 @@
import SwiftUI
struct ContentView: View {
@State private var selectedTab: Int = 1
@StateObject var vm = ScheduleViewModel()
@State private var selectedTab: TabBarModel = .schedule
@State private var isTabBarHidden = false
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
var body: some View {
ZStack (alignment: .bottom) {
TabView(selection: $selectedTab) {
Text("Tasks")
.tabItem {
Image(systemName: "books.vertical")
Text("Задания")
}
.tag(0)
.tag(TabBarModel.tasks)
MainView(vm: vm)
.tabItem {
Image(systemName: "house")
Text("Расписание")
MainView(vm: vm, networkMonitor: networkMonitor)
.tag(TabBarModel.schedule)
.background {
if !isTabBarHidden {
HideTabBar {
print("TabBar is hidden")
isTabBarHidden = true
}
}
}
.tag(1)
Text("Settings")
.tabItem {
Image(systemName: "gear")
Text("Настройки")
SettingsView(vm: vm, networkMonitor: networkMonitor)
.tag(TabBarModel.settings)
}
.tag(2)
TabBarView(selectedTab: $selectedTab)
}
.accentColor(Color("blueColor"))
.onAppear {
vm.fetchWeekSchedule()
}
}
}
struct HideTabBar: UIViewRepresentable {
var result: () -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
DispatchQueue.main.async {
if let tabController = view.tabController {
tabController.tabBar.isHidden = true
result()
}
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
extension UIView {
var tabController: UITabBarController? {
if let controller = sequence(first: self, next: {
$0.next
}).first(where: { $0 is UITabBarController}) as? UITabBarController {
return controller
}
return nil
}
}
#Preview {
ContentView()
@Previewable @StateObject var vm1 = ScheduleViewModel()
@Previewable @StateObject var vm2 = NetworkMonitor()
ContentView(vm: vm1, networkMonitor: vm2)
}

View File

@ -0,0 +1,31 @@
//
// NetworkErrorView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 26.03.2025.
//
import SwiftUI
struct NetworkErrorView: View {
var message: String
var body: some View {
VStack {
Spacer()
VStack {
Image(systemName: "wifi.slash")
.font(.system(size: 60, weight: .light))
.frame(width: 70, height: 70)
Text(message)
.font(.custom("Montserrat-Medium", fixedSize: 15))
.padding(.top, 5)
}
.padding(.horizontal, 30)
Spacer()
}
}
}
#Preview {
NetworkErrorView(message: "Восстановите подключение к интернету чтобы мы смогли загрузить расписание")
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,53 @@
import SwiftUI
struct LoadingScheduleView: View {
@State private var isAnimated = false
var body: some View {
ZStack {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 20) {
ForEach(0..<5, 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)
.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(height: 70)
.padding(.horizontal, 20)
.animation(.linear(duration: 0.8).repeatForever(autoreverses: true), value: isAnimated)
}
}
}
.onAppear {
isAnimated.toggle()
}
.padding(.top, 10)
}
}
}
}
#Preview {
LoadingScheduleView()
}

View File

@ -0,0 +1,29 @@
//
// LoadingView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 04.04.2025.
//
import SwiftUI
struct LoadingView: View {
@State private var isAnimating = false
var body: some View {
Circle()
.trim(from: 0.2, to: 1.0)
.stroke(Color("blueColor"), lineWidth: 3)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isAnimating ? 360 : 0))
.animation(
Animation.linear(duration: 0.6).repeatForever(autoreverses: false),
value: isAnimating
)
.onAppear { isAnimating = true }
}
}
#Preview {
LoadingView()
}

View File

@ -0,0 +1,52 @@
//
// CreatedClassView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 23.12.2024.
//
import SwiftUI
struct CreatedClassView: View {
@ObservedObject var _class: CoreDataClassModel
var provider = ClassProvider.shared
var body: some View {
let existingCopy = try? provider.viewContext.existingObject(with: _class.objectID)
if existingCopy != nil {
HStack(spacing: 15) {
VStack {
Text(getTimeString(_class.starttime))
.font(.custom("Montserrat-Regular", fixedSize: 15))
.padding(.bottom, 1)
Text(getTimeString(_class.endtime))
.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(_class.important ? Color("redForImportant") : onlineOrNot(_class.online))
Text(getSubjectName(_class.subject, _class.professor, _class.auditory))
.font(.custom("Montserrat-Medium", fixedSize: 15))
.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)
}
}
}
#Preview {
CreatedClassView(_class: .preview())
}

View File

@ -0,0 +1,47 @@
//
// AuditoryFieldView.swift
// Schedule ICTIS
//
// Created by G412 on 23.01.2025.
//
import SwiftUI
struct AuditoryFieldView: View {
@Binding var text: String
var labelForField: String
@FocusState var isFocused: Bool
var body: some View {
HStack(spacing: 0) {
Image(systemName: "mappin.and.ellipse")
.foregroundColor(Color.gray)
.padding(.leading, 12)
.padding(.trailing, 14)
TextField(labelForField, text: $text)
.font(.custom("Montserrat-Meduim", fixedSize: 17))
.disableAutocorrection(true)
.submitLabel(.done)
.focused($isFocused)
if isFocused {
Button {
self.text = ""
self.isFocused = false
} label: {
Image(systemName: "xmark.circle.fill")
.padding(.trailing, 20)
.offset(x: 10)
.foregroundColor(.gray)
}
}
}
.frame(height: 40)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
}
}
#Preview {
AuditoryFieldView(text: .constant(""), labelForField: "Корпус-аудитория")
}

View File

@ -9,11 +9,12 @@ import SwiftUI
struct CommentFieldView: View {
@Binding var textForComment: String
@FocusState private var isFocused: Bool
@FocusState var isFocused: Bool
var body: some View {
HStack {
TextField("Комментарий", text: $textForComment)
.font(.custom("Montserrat-Medium", fixedSize: 17))
.submitLabel(.done)
.multilineTextAlignment(.leading)
.focused($isFocused)

View File

@ -1,25 +1,24 @@
//
// Field.swift
// ProfessorFieldView.swift
// Schedule ICTIS
//
// Created by G412 on 16.12.2024.
// Created by G412 on 23.01.2025.
//
import SwiftUI
struct ProfessorAuditoryClassFieldView: View {
struct ProfessorFieldView: View {
@Binding var text: String
var nameOfImage: String
var labelForField: String
@FocusState private var isFocused: Bool
@FocusState var isFocused: Bool
var body: some View {
HStack(spacing: 0) {
Image(systemName: nameOfImage)
Image(systemName: "graduationcap")
.foregroundColor(Color.gray)
.padding(.leading, 12)
.padding(.trailing, 7)
TextField(labelForField, text: $text)
.font(.system(size: 18, weight: .regular))
.font(.custom("Montserrat-Meduim", fixedSize: 17))
.disableAutocorrection(true)
.submitLabel(.done)
.focused($isFocused)
@ -44,5 +43,5 @@ struct ProfessorAuditoryClassFieldView: View {
}
#Preview {
ContentView()
ProfessorFieldView(text: .constant(""), labelForField: "Преподаватель")
}

View File

@ -8,26 +8,28 @@
import SwiftUI
struct StartEndTimeFieldView: View {
@Binding var isIncorrectDate: Bool
@Binding var selectedDay: Date
@Binding var selectedTime: Date
var imageName: String
var text: String
@State private var isTimeSelected: Bool = false
@Binding var isTimeSelected: Bool
var body: some View {
HStack {
Image(systemName: imageName)
.foregroundColor(Color("grayForFields"))
.foregroundColor(isIncorrectDate ? .red : Color("grayForFields"))
.padding(.leading, 12)
.padding(.trailing, 5)
if !isTimeSelected {
if !isTimeSelected || isIncorrectDate {
Text(text)
.font(.system(size: 17, weight: .regular))
.font(.custom("Montserrat-Meduim", fixedSize: 17))
.foregroundColor(.gray.opacity(0.5))
}
if isTimeSelected {
else {
Text("\(selectedTime, formatter: timeFormatter)")
.foregroundColor(.black)
.font(.system(size: 17, weight: .medium))
.foregroundColor(isIncorrectDate ? .red : .black)
.font(.custom("Montserrat-Medium", fixedSize: 17))
.padding(.trailing, 10)
}
Spacer()
@ -38,6 +40,7 @@ struct StartEndTimeFieldView: View {
.fill(.white)
)
.overlay {
if selectedDay.isToday {
DatePicker("", selection: $selectedTime, in: Date()..., displayedComponents: .hourAndMinute)
.padding(.trailing, 35)
.blendMode(.destinationOver)
@ -45,9 +48,14 @@ struct StartEndTimeFieldView: View {
isTimeSelected = true
}
}
else {
DatePicker("", selection: $selectedTime, displayedComponents: .hourAndMinute)
.padding(.trailing, 35)
.blendMode(.destinationOver)
.onChange(of: selectedTime) { newValue, oldValue in
isTimeSelected = true
}
}
}
}
#Preview {
StartEndTimeFieldView(selectedTime: .constant(Date()), imageName: "clock", text: "Начало")
}

View File

@ -0,0 +1,65 @@
//
// Field.swift
// Schedule ICTIS
//
// Created by G412 on 16.12.2024.
// КТбо2-6
import SwiftUI
struct SubjectFieldView: View {
@Binding var text: String
@Binding var isShowingSubjectFieldRed: Bool
@Binding var labelForField: String
@FocusState var isFocused: Bool
var body: some View {
HStack(spacing: 0) {
Image(systemName: "book")
.foregroundColor(Color.gray)
.padding(.leading, 12)
.padding(.trailing, 9)
TextField(labelForField, text: $text)
.font(.custom("Montserrat-Meduim", fixedSize: 17))
.disableAutocorrection(true)
.submitLabel(.done)
.focused($isFocused)
.onChange(of: isFocused, initial: false) { oldValue, newValue in
if newValue {
self.isShowingSubjectFieldRed = false
self.labelForField = "Предмет"
}
}
.background {
Group {
if isShowingSubjectFieldRed {
Text("Поле должно быть заполнено!")
.font(.custom("Montserrat-Meduim", fixedSize: 17))
.foregroundColor(.red)
.frame(width: 290)
.padding(.leading, -38)
}
}
}
if isFocused {
Button {
self.text = ""
self.isFocused = false
} label: {
Image(systemName: "xmark.circle.fill")
.padding(.trailing, 20)
.offset(x: 10)
.foregroundColor(.gray)
}
}
}
.frame(height: 40)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
}
}
#Preview {
SubjectFieldView(text: .constant(""), isShowingSubjectFieldRed: .constant(false), labelForField: .constant("Предмет"))
}

View File

@ -0,0 +1,50 @@
//
// 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

@ -2,7 +2,7 @@
// FirstLaunchScheduleView.swift
// Schedule ICTIS
//
// Created by G412 on 06.12.2024.
// Created by Mironov Egor on 06.12.2024.
//
import SwiftUI

View File

@ -1,25 +0,0 @@
//
// LoadingView.swift
// Schedule ICTIS
//
// Created by G412 on 11.12.2024.
//
import SwiftUI
struct LoadingView: View {
@Binding var isLoading: Bool
var body: some View {
ZStack {
Color("background")
.ignoresSafeArea()
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .secondary))
.scaleEffect(1)
}
}
}
#Preview {
LoadingView(isLoading: .constant(true))
}

View File

@ -10,26 +10,33 @@ import SwiftUI
struct MainView: View {
@State private var searchText: String = ""
@State private var isShowingMonthSlider: Bool = false
@State private var isFirstAppearence = true
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
@FocusState private var isFocusedSearchBar: Bool
@State private var isScrolling: Bool = false
var body: some View {
VStack {
SearchBarView(text: $searchText, vm: vm)
if (vm.isFirstStartOffApp && vm.isLoading) {
LoadingView(isLoading: $vm.isLoading)
SearchBarView(text: $searchText, isFocused: _isFocusedSearchBar, vm: vm, isShowingMonthSlider: $isShowingMonthSlider)
.onChange(of: isScrolling, initial: false) { oldValue, newValue in
if newValue && isScrolling {
isFocusedSearchBar = false
}
else if (vm.isFirstStartOffApp) {
FirstLaunchScheduleView()
}
CurrentDateView()
FilterGroupsView(vm: vm)
if vm.isLoading {
LoadingScheduleView()
}
else {
CurrentDateView()
ScheduleView(vm: vm)
ScheduleView(vm: vm, networkMonitor: networkMonitor, isScrolling: $isScrolling)
}
}
.alert(isPresented: $vm.isShowingAlertForIncorrectGroup, error: vm.errorInNetwork) { error in
Button("ОК") {
print("This alert")
vm.isShowingAlertForIncorrectGroup = false
vm.errorInNetwork = nil
}
} message: { error in
Text(error.failureReason)
}
@ -42,14 +49,14 @@ struct MainView: View {
HStack {
VStack (alignment: .leading, spacing: 0) {
Text(vm.selectedDay.format("EEEE"))
.font(.system(size: 40, weight: .semibold))
.font(.custom("Montserrat-SemiBold", fixedSize: 30))
.foregroundStyle(.black)
HStack (spacing: 5) {
Text(vm.selectedDay.format("dd"))
.font(.system(size: 20, weight: .bold))
.font(.custom("Montserrat-Bold", fixedSize: 17))
.foregroundStyle(Color("grayForDate"))
Text(vm.selectedDay.format("MMMM"))
.font(.system(size: 20, weight: .bold))
.font(.custom("Montserrat-Bold", fixedSize: 17))
.foregroundStyle(Color("grayForDate"))
Spacer()
Button(action: {
@ -59,7 +66,7 @@ struct MainView: View {
}) {
HStack(spacing: 2) {
Text(isShowingMonthSlider ? "Свернуть" : "Развернуть")
.font(.system(size: 16, weight: .light))
.font(.custom("Montserrat-Regular", fixedSize: 15))
.foregroundStyle(Color.blue)
Image(isShowingMonthSlider ? "arrowup" : "arrowdown")
.resizable()
@ -76,16 +83,15 @@ struct MainView: View {
if (!isShowingMonthSlider) {
WeekTabView(vm: vm)
.transition(.opacity)
.animation(.easeInOut(duration: 0.25), value: isShowingMonthSlider)
}
else {
MonthTabView(vm: vm)
.transition(.opacity)
.animation(.linear(duration: 0.5), value: isShowingMonthSlider)
}
}
.padding(.horizontal)
.animation(.easeInOut(duration: 0.25), value: isShowingMonthSlider)
}
}
#Preview {
ContentView()
}

View File

@ -2,7 +2,7 @@
// NoScheduleView.swift
// Schedule ICTIS
//
// Created by G412 on 12.12.2024.
// Created by Mironov Egor on 12.12.2024.
//
import SwiftUI
@ -11,8 +11,9 @@ struct NoScheduleView: View {
var body: some View {
VStack {
ScrollView (showsIndicators: false) {
Text("Пока расписания нет")
.padding(.top, 20)
Text("Пока что расписания нет😪")
.padding(.top, 100)
.font(.custom("Montserrat-SemiBold", fixedSize: 17))
}
}
}

View File

@ -1,118 +1,262 @@
//
// ScheduleView.swift
// Schedule ICTIS
//
// Created by G412 on 05.12.2024.
//
import SwiftUI
import CoreData
struct ScheduleView: View {
@State private var isShowingSheet: Bool = false
@ObservedObject var vm: ScheduleViewModel
@FetchRequest(fetchRequest: ClassModel.all()) private var classes
@ObservedObject var networkMonitor: NetworkMonitor
@FetchRequest(fetchRequest: CoreDataClassModel.all()) private var classes // Список пар добавленных пользователем
@FetchRequest(fetchRequest: JsonClassModel.all()) private var subjects // Список пар сохраненных в CoreData
@State private var selectedClass: CoreDataClassModel? = nil
@State private var lastOffset: CGFloat = 0
@State private var scrollTimer: Timer? = nil
@Binding var isScrolling: Bool
var provider = ClassProvider.shared
private var hasSubjectsToShow: Bool {
subjects.contains { subject in
subject.week == vm.week
}
}
private var hasClassesToShow: Bool {
classes.contains { _class in
_class.day == vm.selectedDay
}
}
var body: some View {
if vm.isLoading {
LoadingView(isLoading: $vm.isLoading)
}
else {
if vm.errorInNetwork != .invalidResponse {
ZStack(alignment: .top) {
ScrollView(.vertical, showsIndicators: false) {
VStack (spacing: 20) {
ForEach(vm.classes.indices, id: \.self) { index in
if index != 0 && index != 1 && index == vm.selectedIndex {
let daySchedule = vm.classes[index] // Это массив строк для дня
ForEach(daySchedule.indices.dropFirst(), id: \.self) { lessonIndex in
let lesson = daySchedule[lessonIndex] // Это строка с расписанием одной пары
if !lesson.isEmpty {
HStack(spacing: 10) {
VStack {
Text(convertTimeString(vm.classes[1][lessonIndex])[0])
.font(.system(size: 15, weight: .regular))
Text(convertTimeString(vm.classes[1][lessonIndex])[1])
.font(.system(size: 15, weight: .regular))
if networkMonitor.isConnected {
onlineContent
} else {
offlineContent
}
.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(lesson))
Text(lesson)
.font(.system(size: 18, weight: .regular))
.padding(.top, 7)
.padding(.bottom, 7)
Spacer()
gradientOverlay
}
.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)
.onTapGesture {
isShowingSheet = true
.onAppear {
deleteClassesFormCoreDataIfMonday()
if networkMonitor.isConnected {
checkSavingOncePerDay()
}
}
.sheet(item: $selectedClass, onDismiss: { selectedClass = nil }) { _class in
CreateEditClassView(vm: .init(provider: provider, _class: _class), day: vm.selectedDay)
}
}
}
ForEach(classes) { _class in
if datesAreEqual(_class.day, vm.selectedDay) {
HStack(spacing: 10) {
VStack {
Text(getTimeString(_class.starttime))
.font(.system(size: 15, weight: .regular))
Text(getTimeString(_class.endtime))
.font(.system(size: 15, weight: .regular))
}
.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(_class.important ? Color("redForImportant") : onlineOrNot(_class.online))
Text(getSubjectName(_class.subject, _class.professor, _class.auditory))
.font(.system(size: 18, weight: .regular))
.padding(.top, 7)
.padding(.bottom, 7)
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)
.onTapGesture {
isShowingSheet = true
}
}
}
}
.frame(width: UIScreen.main.bounds.width)
.padding(.bottom, 100)
.padding(.top, 30)
}
VStack {
LinearGradient(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)
}
.sheet(isPresented: $isShowingSheet) {
SheetChangeClassView(isShowingSheet: $isShowingSheet)
}
}
else {
// Онлайн-контент (с интернетом)
private var onlineContent: some View {
Group {
if vm.errorInNetwork == .timeout {
NetworkErrorView(message: "Проверьте подключение к интернету")
} else if vm.isLoading {
LoadingScheduleView()
} else if vm.errorInNetwork != .invalidResponse {
scheduleScrollView(isOnline: true)
} else {
NoScheduleView()
}
}
.onAppear {
if vm.classesGroups.isEmpty {
vm.fetchWeekSchedule()
}
}
}
#Preview {
ContentView()
// Оффлайн-контент (без интернета)
private var offlineContent: some View {
scheduleScrollView(isOnline: false)
}
// Общий ScrollView для расписания
private func scheduleScrollView(isOnline: Bool) -> 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..<vm.classesGroups.count, id: \.self) { dayIndex in
if dayIndex == vm.selectedIndex {
ForEach(vm.classesGroups[dayIndex]) { info in
if vm.showOnlyChoosenGroup == "Все" || info.group == vm.showOnlyChoosenGroup {
SubjectView(info: ClassInfo(subject: info.subject, group: info.group, time: info.time), vm: vm)
}
}
}
}
} else {
let filteredSubjects = subjects.filter { $0.day == Int16(vm.selectedIndex) }
if (filteredSubjects.isEmpty || vm.week != 0) && !hasClassesToShow {
ConnectingToNetworkView()
.padding(.top, 100)
} 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) }) {
VStack(alignment: .leading, spacing: 20) {
Text("Мои пары")
.font(.custom("Montserrat-Bold", fixedSize: 20))
ForEach(classes) { _class in
if daysAreEqual(_class.day, vm.selectedDay) {
CreatedClassView(_class: _class)
.onTapGesture {
selectedClass = _class
}
}
}
}
}
}
}
// Градиентный оверлей
private var gradientOverlay: some View {
VStack {
LinearGradient(
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)
}
}
extension ScheduleView {
private func deleteClassesFormCoreDataIfMonday() {
let today = Date()
let calendar = Calendar.current
let weekday = calendar.component(.weekday, from: today)
if weekday == 6 {
for _class in classes {
if _class.day < today {
do {
try provider.delete(_class, in: provider.viewContext)
} catch {
print("❌ - Ошибка при удалении: \(error)")
}
}
}
}
}
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 {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}

View File

@ -9,9 +9,10 @@ import SwiftUI
struct SearchBarView: View {
@Binding var text: String
@State private var isEditing = false
@FocusState var isFocused: Bool
@State private var isShowingSheet: Bool = false
@ObservedObject var vm: ScheduleViewModel
@Binding var isShowingMonthSlider: Bool
var provider = ClassProvider.shared
@ -24,26 +25,25 @@ struct SearchBarView: View {
.padding(.trailing, 7)
TextField("Поиск группы", text: $text)
.disableAutocorrection(true)
.onTapGesture {
self.isEditing = true
}
.focused($isFocused)
.onSubmit {
self.isEditing = false
self.isFocused = false
if (!text.isEmpty) {
if !vm.numOfGroup.isEmpty {
}
vm.fetchWeekSchedule(text)
vm.group = text
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 = ""
}
.submitLabel(.search)
if isEditing {
if isFocused {
Button {
self.text = ""
self.isEditing = false
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
self.isFocused = false
} label: {
Image(systemName: "xmark.circle.fill")
.padding(.trailing, 20)
@ -52,15 +52,17 @@ struct SearchBarView: View {
.background(
)
}
.background(Color.red)
}
}
.simultaneousGesture(TapGesture().onEnded {
self.isShowingMonthSlider = false
})
.frame(height: 40)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
if (!vm.isFirstStartOffApp) {
if !isFocused {
Button {
isShowingSheet = true
} label: {
@ -83,11 +85,8 @@ struct SearchBarView: View {
.frame(height: 40)
.accentColor(.blue)
.sheet(isPresented: $isShowingSheet) {
SheetCreateClassView(isShowingSheet: $isShowingSheet, vm: .init(provider: provider))
CreateEditClassView(vm: .init(provider: provider), day: vm.selectedDay)
}
}
}
#Preview {
ContentView()
}

View File

@ -0,0 +1,301 @@
//
// SheetCreateClassView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 12.12.2024.
//
import SwiftUI
struct CreateEditClassView: View {
@Environment(\.dismiss) private var dismiss
@State private var isShowingDatePickerForDate: Bool = false
@ObservedObject var vm: EditClassViewModel
var day: Date
@State private var isIncorrectDate1: Bool = false
@State private var isIncorrectDate2: Bool = false
@State private var isShowingSubjectFieldRed: Bool = false
@State private var isSelectedTime1 = false
@State private var isSelectedTime2 = false
@State private var textForLabelInSubjectField: String = "Предмет"
@State private var selectedType: String = "Оффлайн"
@FocusState private var isFocusedSubject: Bool
@FocusState private var isFocusedAuditory: Bool
@FocusState private var isFocusedProfessor: Bool
@FocusState private var isFocusedComment: Bool
var provider = ClassProvider.shared
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack {
SubjectFieldView(text: $vm._class.subject, isShowingSubjectFieldRed: $isShowingSubjectFieldRed, labelForField: $textForLabelInSubjectField, isFocused: _isFocusedSubject)
.padding(.bottom, 10)
HStack {
HStack {
Text("Тип")
.font(.custom("Montserrat-Medium", fixedSize: 17))
.foregroundColor(.black)
Spacer()
HStack {
Text(vm._class.online)
.font(.custom("Montserrat-Medium", fixedSize: 17))
.foregroundColor(Color("customGray3"))
Image("upDownArrows")
.resizable()
.scaledToFit()
.frame(width: 15, height: 15)
}
.padding(.horizontal)
}
.padding(.horizontal)
.padding(.top, 10)
.padding(.bottom, 10)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.overlay {
HStack {
Spacer()
Picker("Тип", selection: $vm._class.online, content: {
ForEach(MockData.onlineOrOffline, id: \.self) {
Text($0)
}
})
.accentColor(Color("grayForFields"))
.padding(.trailing, 35)
.blendMode(.destinationOver)
}
.frame(width: UIScreen.main.bounds.width)
}
}
.padding(.bottom, 10)
ZStack {
if vm._class.online == "Оффлайн" {
AuditoryFieldView(text: $vm._class.auditory, labelForField: "Корпус-аудитория", isFocused: _isFocusedAuditory)
.padding(.bottom, 10)
.transition(.asymmetric(
insertion: .offset(y: -50).combined(with: .identity),
removal: .offset(y: -50).combined(with: .opacity)
))
}
}
.animation(
vm._class.online == "Оффлайн" ?
.linear(duration: 0.3) : // Анимация для появления
.linear(duration: 0.2), // Анимация для исчезновения
value: vm._class.online
)
ProfessorFieldView(text: $vm._class.professor, labelForField: "Преподаватель", isFocused: _isFocusedProfessor)
.padding(.bottom, 10)
HStack {
Image(systemName: "calendar")
.foregroundColor(Color.gray)
.padding(.leading, 12)
.padding(.trailing, 5)
Text("Дата")
.foregroundColor(Color("grayForFields").opacity(0.5))
.font(.custom("Montserrat-Meduim", fixedSize: 17))
Spacer()
Text("\(vm._class.day, formatter: dateFormatter)")
.foregroundColor(.black)
.font(.custom("Montserrat-Medium", fixedSize: 17))
.padding(.trailing, 20)
}
.frame(height: 40)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.overlay {
DatePicker("", selection: $vm._class.day, in: Date()..., displayedComponents: .date)
.blendMode(.destinationOver)
}
.padding(.bottom, 10)
HStack {
StartEndTimeFieldView(isIncorrectDate: $isIncorrectDate1, selectedDay: $vm._class.day, selectedTime: $vm._class.starttime, imageName: "clock", text: "Начало", isTimeSelected: $isSelectedTime1)
.onChange(of: vm._class.starttime) { oldValue, newValue in
if !checkStartTimeLessThenEndTime(vm._class.starttime, vm._class.endtime) {
self.isIncorrectDate1 = true
self.isSelectedTime1 = false
print("Первый")
print(self.isSelectedTime1)
print(self.isSelectedTime2)
}
else {
self.isIncorrectDate1 = false
self.isIncorrectDate2 = false
}
}
Spacer()
StartEndTimeFieldView(isIncorrectDate: $isIncorrectDate2, selectedDay: $vm._class.day, selectedTime: $vm._class.endtime, imageName: "clock.badge.xmark", text: "Конец", isTimeSelected: $isSelectedTime2)
.onChange(of: vm._class.endtime) { oldValue, newValue in
if !checkStartTimeLessThenEndTime(vm._class.starttime, vm._class.endtime) {
self.isIncorrectDate2 = true
self.isSelectedTime2 = false
print("Второй")
print(self.isSelectedTime1)
print(self.isSelectedTime2)
}
else {
self.isIncorrectDate1 = false
self.isIncorrectDate2 = false
}
}
}
.frame(height: 40)
.padding(.bottom, 10)
Toggle("Пометить как важную", isOn: $vm._class.important)
.font(.custom("Montserrat-Medium", fixedSize: 17))
.frame(height: 40)
.padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.padding(.bottom, 10)
HStack {
HStack {
Text("Напоминание")
.font(.custom("Montserrat-Medium", fixedSize: 17))
.foregroundColor(.black)
Spacer()
HStack {
Text(vm._class.notification)
.font(.custom("Montserrat-Medium", fixedSize: 17))
.foregroundColor(Color("customGray3"))
Image("upDownArrows")
.resizable()
.scaledToFit()
.frame(width: 15, height: 15)
}
.padding(.horizontal)
}
.padding(.horizontal)
.padding(.top, 10)
.padding(.bottom, 10)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.overlay {
HStack {
Spacer()
Picker("", selection: $vm._class.notification , content: {
ForEach(MockData.notifications, id: \.self) {
Text($0)
}
})
.accentColor(Color("grayForFields"))
.padding(.trailing, 35)
.blendMode(.destinationOver)
}
.frame(width: UIScreen.main.bounds.width)
}
}
.padding(.bottom, 10)
CommentFieldView(textForComment: $vm._class.comment, isFocused: _isFocusedComment)
.padding(.bottom, 20)
if !vm.isNew {
Button {
do {
try provider.delete(vm._class, in: provider.viewContext)
dismiss()
} catch {
print(error)
}
} label: {
HStack {
Spacer()
Image(systemName: "trash")
Text("Удалить занятие")
.font(.custom("Montserrat-Medium", fixedSize: 17))
Spacer()
}
.frame(height: 40)
.background(Color.white)
.foregroundColor(Color.red)
.cornerRadius(10)
}
}
Spacer()
}
.padding(.horizontal)
.padding(.bottom, 60)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Отменить") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Сохранить") {
isFocusedSubject = false
isFocusedProfessor = false
isFocusedAuditory = false
isFocusedComment = false
if (vm._class.subject.isEmpty || (isIncorrectDate1 || isIncorrectDate2) || (!isSelectedTime1 || !isSelectedTime2)) {
if (vm._class.subject.isEmpty) {
self.isShowingSubjectFieldRed = true
self.textForLabelInSubjectField = ""
}
if !isSelectedTime1 {
self.isIncorrectDate1 = true
}
if !isSelectedTime2 {
self.isIncorrectDate2 = true
}
}
else {
do {
try vm.save()
dismiss()
} catch {
print(error)
}
}
}
}
}
.navigationTitle(vm.isNew ? "Новая пара" : "Изменить данные")
.background(Color("background"))
.onAppear {
let temp = Calendar.current.date(byAdding: .hour, value: 1, to: Date.init())
if let endTime = temp {
if (!hoursMinutesAreEqual(date1: vm._class.starttime, isEqualTo: Date()) && !hoursMinutesAreEqual(date1: vm._class.endtime, isEqualTo: endTime)) {
self.isSelectedTime1 = true
self.isSelectedTime2 = true
print(vm._class.starttime)
print(vm._class.endtime)
print(endTime)
print(Date())
}
}
if day > Calendar.current.startOfDay(for: Date()) {
vm._class.day = day
}
}
.onTapGesture {
isFocusedSubject = false
isFocusedProfessor = false
isFocusedAuditory = false
isFocusedComment = false
}
}
}
}
#Preview {
let day: Date = .init()
CreateEditClassView(vm: .init(provider: .shared), day: day)
}

View File

@ -1,37 +0,0 @@
//
// SheetView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 12.12.2024.
//
import SwiftUI
struct SheetChangeClassView: View {
@Binding var isShowingSheet: Bool
var body: some View {
NavigationView {
VStack {
Spacer()
Text("Редактирвоание пары")
Spacer()
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Отменить") {
isShowingSheet = false
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Сохранить") {
isShowingSheet = false
}
}
}
}
}
}
#Preview {
SheetChangeClassView(isShowingSheet: .constant(true))
}

View File

@ -1,160 +0,0 @@
//
// SheetCreateClassView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 12.12.2024.
//
import SwiftUI
struct SheetCreateClassView: View {
@Binding var isShowingSheet: Bool
@State private var textForNameOfClass = ""
@State private var textForNameOfAuditory = ""
@State private var textForNameOfProfessor = ""
@State private var isShowingDatePickerForDate: Bool = false
@State private var isImportant: Bool = false
@State private var selectedOptionForNotification: String = "Нет"
@State private var selectedOptionForOnline: String = "Оффлайн"
@State private var textForComment: String = ""
@ObservedObject var vm: EditClassViewModel
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ProfessorAuditoryClassFieldView(text: $vm._class.subject, nameOfImage: "book", labelForField: "Предмет")
.padding(.bottom, 10)
ProfessorAuditoryClassFieldView(text: $vm._class.auditory, nameOfImage: "mappin.and.ellipse", labelForField: "Корпус-аудитория")
.padding(.bottom, 10)
ProfessorAuditoryClassFieldView(text: $vm._class.professor, nameOfImage: "book", labelForField: "Преподаватель")
.padding(.bottom, 10)
HStack {
Image(systemName: "calendar")
.foregroundColor(Color.gray)
.padding(.leading, 12)
.padding(.trailing, 7)
Text("Дата")
.foregroundColor(Color("grayForFields").opacity(0.5))
.font(.system(size: 18, weight: .regular))
Spacer()
Text("\(vm._class.day, formatter: dateFormatter)")
.foregroundColor(.black)
.font(.system(size: 18, weight: .medium))
.padding(.trailing, 20)
}
.frame(height: 40)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.overlay {
DatePicker("", selection: $vm._class.day, in: Date()..., displayedComponents: .date)
.blendMode(.destinationOver)
}
.padding(.bottom, 10)
HStack {
StartEndTimeFieldView(selectedTime: $vm._class.starttime, imageName: "clock", text: "Начало")
.onChange(of: vm._class.starttime) { oldValue, newValue in
if !checkStartTimeLessThenEndTime(vm._class.starttime, vm._class.endtime) {
print("Values \(oldValue) - \(newValue) 1")
print(vm._class.starttime)
vm._class.starttime = oldValue
}
}
Spacer()
StartEndTimeFieldView(selectedTime: $vm._class.endtime, imageName: "clock.badge.xmark", text: "Конец")
.onChange(of: vm._class.endtime) { oldValue, newValue in
print("Values \(oldValue) - \(newValue) 2")
print(vm._class.endtime)
validateTime(old: oldValue, new: newValue, isStartChanged: false)
}
}
.frame(height: 40)
.padding(.bottom, 10)
Toggle("Пометить как важную", isOn: $vm._class.important)
.frame(height: 40)
.padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.padding(.bottom, 10)
HStack {
Text("Напоминанние")
Spacer()
Picker("Напоминание", selection: $vm._class.notification, content: {
ForEach(MockData.notifications, id: \.self) {
Text($0)
}
})
.accentColor(Color("grayForFields"))
}
.frame(height: 40)
.padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.padding(.bottom, 10)
HStack {
Text("Тип")
Spacer()
Picker("Тип", selection: $vm._class.online, content: {
ForEach(MockData.onlineOrOffline, id: \.self) {
Text($0)
}
})
.accentColor(Color("grayForFields"))
}
.frame(height: 40)
.padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.padding(.bottom, 10)
CommentFieldView(textForComment: $vm._class.comment)
Spacer()
}
.padding(.horizontal)
.padding(.bottom, 60)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Отменить") {
isShowingSheet = false
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Сохранить") {
do {
try vm.save()
} catch {
print(error)
}
isShowingSheet = false
}
}
}
.navigationTitle("Новая пара")
.background(Color("background"))
}
}
func validateTime(old oldValue: Date, new newValue: Date, isStartChanged: Bool) {
if !checkStartTimeLessThenEndTime(vm._class.starttime, vm._class.endtime) {
if isStartChanged {
vm._class.starttime = Date()
} else {
vm._class.starttime = Date()
}
print("Invalid time selected. Reverting to old value.")
}
}
}
#Preview {
SheetCreateClassView(isShowingSheet: .constant(true), vm: .init(provider: .shared))
}

View File

@ -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])
}
}
}

View File

@ -8,56 +8,51 @@
import SwiftUI
struct MonthTabView: View {
@State private var currentMonthIndex: Int = 1
@State private var monthSlider: [[Date.MonthWeek]] = []
@State var currentMonthIndex: Int = 1
@State var monthSlider: [[Date.MonthWeek]] = []
@State private var createMonth: Bool = false
@State private var currentWeekIndex: Int = 0
@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(.system(size: 15, weight: .semibold))
.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: {
vm.updateSelectedDayIndex()
if monthSlider.isEmpty {
let currentMonth = Date().fetchMonth(vm.selectedDay)
if let firstDate = currentMonth.first?.week[0].date {
monthSlider.append(firstDate.createPreviousMonth())
.frame(height: 220)
.padding(.top, 26)
.padding(.bottom, 20)
.onAppear {
updateMonthScreenViewForNewGroup()
}
monthSlider.append(currentMonth)
if let lastDate = currentMonth.last?.week[6].date {
monthSlider.append(lastDate.createNextMonth())
}
}
})
.onChange(of: currentMonthIndex, initial: false) { oldValue, newValue in
if newValue == 0 || newValue == (monthSlider.count - 1) {
createMonth = true
}
}
.onChange(of: vm.isNewGroup, initial: false) { oldValue, newValue in
if newValue {
monthSlider.removeAll()
currentMonthIndex = 1
updateMonthScreenViewForNewGroup()
vm.isNewGroup = false
}
}
}
@ViewBuilder
@ -65,7 +60,7 @@ struct MonthTabView: View {
VStack (spacing: 10) {
ForEach(month.indices, id: \.self) { index in
let week = month[index].week
WeekView(week)
WeekViewForMonth(week: week, vm: vm)
}
}
.background {
@ -84,98 +79,6 @@ struct MonthTabView: View {
}
}
}
@ViewBuilder
func WeekView(_ week: [Date.WeekDay]) -> some View {
HStack (spacing: 23) {
ForEach(week) { day in
VStack {
Text(day.date.format("dd"))
.font(.system(size: 15, weight: .bold))
.foregroundStyle(isDateInCurrentMonth(day.date) ? isSameDate(day.date, vm.selectedDay) ? Color.white : Color.black: isSameDate(day.date, vm.selectedDay) ? Color.white : Color("greyForDaysInMonthTabView"))
}
.frame(width: 30, height: 30, alignment: .center)
.background( content: {
Group {
if isSameDate(day.date, vm.selectedDay) {
Color("blueColor")
}
else {
Color("background")
}
if isSameDate(day.date, vm.selectedDay) {
Color("blueColor")
}
}
}
)
.overlay (
Group {
if day.date.isToday && !isSameDate(day.date, vm.selectedDay) {
RoundedRectangle(cornerRadius: 100)
.stroke(Color("blueColor"), lineWidth: 2)
}
}
)
.cornerRadius(15)
.onTapGesture {
if isSameWeek(day.date, vm.selectedDay) {
print("На одной неделе")
}
else {
var difBetweenWeeks = weeksBetween(startDate: vm.selectedDay, endDate: day.date)
if day.date < vm.selectedDay {
difBetweenWeeks = difBetweenWeeks * -1
}
print(difBetweenWeeks)
vm.fetchWeekSchedule("", difBetweenWeeks)
}
vm.selectedDay = day.date
vm.updateSelectedDayIndex()
}
}
}
}
func paginateMonth(_ indexOfWeek: Int = 0) {
let calendar = Calendar.current
if monthSlider.indices.contains(currentMonthIndex) {
if let firstDate = monthSlider[currentMonthIndex].first?.week[0].date,
currentMonthIndex == 0 {
// switch (vm.numOfGroup) {
// case "":
// vm.week -= 1
// default:
// vm.fetchWeekSchedule("new week", -1)
// }
monthSlider.insert(firstDate.createPreviousMonth(), at: 0)
monthSlider.removeLast()
currentMonthIndex = 1
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -5, to: vm.selectedDay) ?? Date.init()
vm.updateSelectedDayIndex()
vm.fetchWeekSchedule("", -5)
}
if let lastDate = monthSlider[currentMonthIndex].last?.week[6].date,
currentMonthIndex == (monthSlider.count - 1) {
// switch (vm.numOfGroup) {
// case "":
// vm.week += 1
// default:
// vm.fetchWeekSchedule("new week", 1)
// }
monthSlider.append(lastDate.createNextMonth())
monthSlider.removeFirst()
currentMonthIndex = monthSlider.count - 2
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 5, to: vm.selectedDay) ?? Date.init()
vm.updateSelectedDayIndex()
vm.fetchWeekSchedule("", 5)
}
}
}
}
#Preview {
ContentView()
}

View File

@ -2,14 +2,14 @@
// WeekTabView.swift
// Schedule ICTIS
//
// Created by G412 on 10.12.2024.
// Created by Mironov Egor on 10.12.2024.
//
import SwiftUI
struct WeekTabView: View {
@State private var currentWeekIndex: Int = 1
@State private var weekSlider: [[Date.WeekDay]] = []
@State var weekSlider: [[Date.WeekDay]] = []
@State private var createWeek: Bool = false
@ObservedObject var vm: ScheduleViewModel
var body: some View {
@ -17,7 +17,7 @@ struct WeekTabView: View {
TabView(selection: $currentWeekIndex) {
ForEach(weekSlider.indices, id: \.self) { index in
let week = weekSlider[index]
WeekView(week)
WeekViewForWeek(weekSlider: $weekSlider, currentWeekIndex: $currentWeekIndex, createWeek: $createWeek, week: week, vm: vm)
.padding(.horizontal, 15)
.tag(index)
}
@ -27,126 +27,20 @@ struct WeekTabView: View {
.frame(height: 90)
}
.onAppear(perform: {
vm.updateSelectedDayIndex()
if weekSlider.isEmpty {
let currentWeek = Date().fetchWeek(vm.selectedDay)
if let firstDate = currentWeek.first?.date {
weekSlider.append(firstDate.createPrevioustWeek())
}
weekSlider.append(currentWeek)
if let lastDate = currentWeek.last?.date {
weekSlider.append(lastDate.createNextWeek())
}
}
updateWeekScreenViewForNewGroup()
})
.onChange(of: currentWeekIndex, initial: false) { oldValue, newValue in
if newValue == 0 || newValue == (weekSlider.count - 1) {
createWeek = true
}
}
}
@ViewBuilder
func WeekView(_ week: [Date.WeekDay]) -> some View {
HStack (spacing: 10) {
ForEach(week) { day in
VStack (spacing: 1) {
Text(day.date.format("E"))
.font(.system(size: 15, weight: .semibold))
.foregroundColor(day.date.format("E") == "Вс" ? Color(.red) : isSameDate(day.date, vm.selectedDay) ? Color("customGray1") : Color("customGray3"))
.padding(.top, 13)
.foregroundColor(.gray)
Text(day.date.format("dd"))
.font(.system(size: 15, weight: .bold))
.foregroundStyle(isSameDate(day.date, vm.selectedDay) ? .white : .black)
.padding(.bottom, 13)
}
.frame(width: 43, height: 55, alignment: .center)
.background( content: {
Group {
if isSameDate(day.date, vm.selectedDay) {
Color("blueColor")
}
else {
Color(.white)
}
if isSameDate(day.date, vm.selectedDay) {
Color("blueColor")
}
}
}
)
.overlay (
Group {
if day.date.isToday && !isSameDate(day.date, vm.selectedDay) {
RoundedRectangle(cornerRadius: 15)
.stroke(Color("blueColor"), lineWidth: 2)
}
}
)
.cornerRadius(15)
.onTapGesture {
vm.selectedDay = day.date
vm.updateSelectedDayIndex()
}
}
}
.background {
GeometryReader {
let minX = $0.frame(in: .global).minX
Color.clear
.preference(key: OffsetKey.self, value: minX)
.onPreferenceChange(OffsetKey.self) { value in
if value.rounded() == 15 && createWeek {
paginateWeek()
createWeek = false
}
}
}
}
}
func paginateWeek() {
let calendar = Calendar.current
if weekSlider.indices.contains(currentWeekIndex) {
if let firstDate = weekSlider[currentWeekIndex].first?.date,
currentWeekIndex == 0 {
switch (vm.numOfGroup) {
case "":
vm.week -= 1
default:
vm.fetchWeekSchedule("new week", -1)
}
weekSlider.insert(firstDate.createPrevioustWeek(), at: 0)
weekSlider.removeLast()
.onChange(of: vm.isNewGroup, initial: false) { oldValue, newValue in
if newValue {
weekSlider.removeAll()
currentWeekIndex = 1
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -1, to: vm.selectedDay) ?? Date.init()
vm.updateSelectedDayIndex()
}
if let lastDate = weekSlider[currentWeekIndex].last?.date,
currentWeekIndex == (weekSlider.count - 1) {
switch (vm.numOfGroup) {
case "":
vm.week += 1
default:
vm.fetchWeekSchedule("new week", 1)
}
weekSlider.append(lastDate.createNextWeek())
weekSlider.removeFirst()
currentWeekIndex = weekSlider.count - 2
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 1, to: vm.selectedDay) ?? Date.init()
vm.updateSelectedDayIndex()
updateWeekScreenViewForNewGroup()
vm.isNewGroup = false
}
}
}
}
#Preview {
ContentView()
}

View File

@ -0,0 +1,32 @@
//
// WeekViewForMonth.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 20.12.2024.
//
import SwiftUI
struct WeekViewForMonth: View {
let week: [Date.WeekDay]
@ObservedObject var vm: ScheduleViewModel
var body: some View {
HStack(spacing: 23) {
ForEach(week) { day in
VStack {
Text(day.date.format("dd"))
.font(.custom("Montserrat-SemiBold", fixedSize: 15))
.foregroundStyle(getForegroundColor(day: day))
}
.frame(width: 30, height: 30, alignment: .center)
.background(getBackgroundColor(day: day))
.overlay(overlay(day: day))
.cornerRadius(15)
.onTapGesture {
handleTap(day: day)
}
}
}
}
}

View File

@ -0,0 +1,76 @@
//
// WeekView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 20.12.2024.
//
import SwiftUI
struct WeekViewForWeek: View {
@Binding var weekSlider: [[Date.WeekDay]]
@Binding var currentWeekIndex: Int
@Binding var createWeek: Bool
let week: [Date.WeekDay]
@ObservedObject var vm: ScheduleViewModel
var body: some View {
HStack (spacing: 10) {
ForEach(week) { day in
VStack (spacing: 1) {
Text(day.date.format("E"))
.font(.custom("Montserrat-SemiBold", fixedSize: 15))
.foregroundColor(day.date.format("E") == "Вс" ? Color(.red) : isSameDate(day.date, vm.selectedDay) ? Color("customGray1") : Color("customGray3"))
.padding(.top, 13)
.foregroundColor(.gray)
Text(day.date.format("dd"))
.font(.custom("Montserrat-Semibold", fixedSize: 15))
.foregroundStyle(isSameDate(day.date, vm.selectedDay) ? .white : .black)
.padding(.bottom, 13)
}
.frame(width: 43, height: 55, alignment: .center)
.background( content: {
Group {
if isSameDate(day.date, vm.selectedDay) {
Color("blueColor")
}
else {
Color(.white)
}
if isSameDate(day.date, vm.selectedDay) {
Color("blueColor")
}
}
}
)
.overlay (
Group {
if day.date.isToday && !isSameDate(day.date, vm.selectedDay) {
RoundedRectangle(cornerRadius: 15)
.stroke(Color("blueColor"), lineWidth: 2)
}
}
)
.cornerRadius(15)
.onTapGesture {
vm.selectedDay = day.date
vm.updateSelectedDayIndex()
}
}
}
.background {
GeometryReader {
let minX = $0.frame(in: .global).minX
Color.clear
.preference(key: OffsetKey.self, value: minX)
.onPreferenceChange(OffsetKey.self) { value in
if value.rounded() == 15 && createWeek {
paginateWeek()
createWeek = false
}
}
}
}
}
}

View File

@ -2,7 +2,7 @@
// MockData.swift
// Schedule ICTIS
//
// Created by G412 on 06.12.2024.
// Created by Mironov Egor on 06.12.2024.
//
import Foundation
@ -15,4 +15,10 @@ struct MockData {
static let notifications = ["Нет", "За 10 минут", "За 30 миннут", "За 1 час"]
static let onlineOrOffline = ["Оффлайн", "Онлайн"]
static let themes = ["Светлая", "Темная", "Системная"]
static let languages = ["Русский", "Английский", "Китайский", "Испанский"]
static let groups = ["КТбо2-6", "КТбо1-9", "КТбо3-3", "ВУЦ", "КТао1-1", "КТсо2-2"]
}

View File

@ -1,57 +0,0 @@
//
// Class.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 18.12.2024.
//
import Foundation
import CoreData
final class ClassModel: NSManagedObject, Identifiable {
@NSManaged var auditory: String
@NSManaged var professor: String
@NSManaged var subject: String
@NSManaged var comment: String
@NSManaged var notification: String
@NSManaged var day: Date
@NSManaged var starttime: Date
@NSManaged var endtime: Date
@NSManaged var important: Bool
@NSManaged var online: String
// Здесь мы выполняем дополнительную инициализацию, назначая значения по умолчанию
override func awakeFromInsert() {
super.awakeFromInsert()
let calendar = Calendar.current
let endTime = calendar.date(byAdding: .hour, value: 1, to: Date.init())
setPrimitiveValue("", forKey: "auditory")
setPrimitiveValue("", forKey: "professor")
setPrimitiveValue("", forKey: "subject")
setPrimitiveValue("", forKey: "comment")
setPrimitiveValue("Нет", forKey: "notification")
setPrimitiveValue(false, forKey: "important")
setPrimitiveValue("Оффлайн", forKey: "online")
setPrimitiveValue(Date.init(), forKey: "day")
setPrimitiveValue(Date.init(), forKey: "starttime")
setPrimitiveValue(endTime, forKey: "endtime")
}
}
// Расширение для загрузки данных из памяти
extension ClassModel {
private static var classesFetchRequest: NSFetchRequest<ClassModel> {
NSFetchRequest(entityName: "ClassModel")
}
// Получаем все данные из памяти
static func all() -> NSFetchRequest<ClassModel> {
let request: NSFetchRequest<ClassModel> = classesFetchRequest
request.sortDescriptors = [
NSSortDescriptor(keyPath: \ClassModel.day, ascending: true)
]
return request
}
}

View File

@ -0,0 +1,91 @@
//
// Class.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 18.12.2024.
//
import Foundation
import CoreData
final class CoreDataClassModel: NSManagedObject, Identifiable {
@NSManaged var auditory: String
@NSManaged var professor: String
@NSManaged var subject: String
@NSManaged var comment: String
@NSManaged var notification: String
@NSManaged var day: Date
@NSManaged var starttime: Date
@NSManaged var endtime: Date
@NSManaged var important: Bool
@NSManaged var online: String
static var dateNow: Date = .now
// Здесь мы выполняем дополнительную инициализацию, назначая значения по умолчанию
// Этот метод вызывается всякий раз, когда объект Core Data вставляется в контекст
override func awakeFromInsert() {
super.awakeFromInsert()
let moscowTimeZone = TimeZone(identifier: "Europe/Moscow")!
var calendar = Calendar.current
calendar.timeZone = moscowTimeZone
let startTime = Date()
let endTime = calendar.date(byAdding: .hour, value: 1, to: Date.init())
setPrimitiveValue("", forKey: "auditory")
setPrimitiveValue("", forKey: "professor")
setPrimitiveValue("", forKey: "subject")
setPrimitiveValue("", forKey: "comment")
setPrimitiveValue("Нет", forKey: "notification")
setPrimitiveValue(false, forKey: "important")
setPrimitiveValue("Оффлайн", forKey: "online")
setPrimitiveValue(startTime, forKey: "day")
setPrimitiveValue(startTime, forKey: "starttime")
setPrimitiveValue(endTime, forKey: "endtime")
}
}
// Расширение для загрузки данных из памяти
extension CoreDataClassModel {
// Получаем все данные из памяти
private static var classesFetchRequest: NSFetchRequest<CoreDataClassModel> {
NSFetchRequest(entityName: "CoreDataClassModel")
}
// Получаем все данные и сортируем их по дню
// Этот метод будет использоваться на View(ScheduleView), где отображаются пары
static func all() -> NSFetchRequest<CoreDataClassModel> {
let request: NSFetchRequest<CoreDataClassModel> = classesFetchRequest
request.sortDescriptors = [
NSSortDescriptor(keyPath: \CoreDataClassModel.day, ascending: true)
]
return request
}
}
extension CoreDataClassModel {
@discardableResult
static func makePreview(count: Int, in context: NSManagedObjectContext) -> [CoreDataClassModel] {
var classes = [CoreDataClassModel]()
for i in 0..<count {
let _class = CoreDataClassModel(context: context)
_class.subject = "Предмет \(i)"
_class.auditory = "Аудитория \(i)"
_class.professor = "Преподаватель \(i)"
_class.day = Calendar.current.date(byAdding: .day, value: i, to: .now) ?? .now
_class.starttime = Date()
_class.endtime = Calendar.current.date(byAdding: .hour, value: i, to: .now) ?? .now
classes.append(_class)
}
return classes
}
static func preview(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel {
return makePreview(count: 1, in: context)[0]
}
static func empty(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel {
return CoreDataClassModel(context: context)
}
}

View File

@ -0,0 +1,20 @@
//
// SubjectsModel.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 19.02.2025.
//
import Foundation
// MARK: - Welcome
struct Welcome: Decodable {
let choices: [Subject]
}
// MARK: - Choice
struct Subject: Decodable, Identifiable {
let name: String
let id: String
let group: String
}

View File

@ -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<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

@ -21,3 +21,10 @@ struct Table: Decodable {
let table: [[String]]
let link: String
}
struct ClassInfo: Identifiable {
let id = UUID()
let subject: String
let group: String
let time: String
}

View File

@ -8,7 +8,7 @@
import SwiftUI
enum TabBarModel: String, CaseIterable {
case schedule = "house"
case tasks = "books.vertical"
case schedule = "house"
case settings = "gear"
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "arrowRight.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 6L15 12L9 18" stroke="#8B8B8B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "arrow.png",
"filename" : "arrowdown.svg",
"idiom" : "universal",
"scale" : "1x"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 1L7 7L1 1" stroke="#007AFF" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 191 B

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Vector.png",
"filename" : "arrowup.svg",
"idiom" : "universal",
"scale" : "1x"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 7L7 1L13 7" stroke="#007AFF" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 191 B

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "auditoryImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "bookImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x89",
"green" : "0x89",
"red" : "0x89"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x89",
"green" : "0x89",
"red" : "0x89"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "professorHatImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "upDownArrows.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,4 @@
<svg width="14" height="18" viewBox="0 0 14 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 7L7 1L13 7" stroke="#878787" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 11L7 17L1 11" stroke="#878787" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 320 B

View File

@ -7,6 +7,7 @@
import Foundation
import CoreData
import SwiftUI
// Это класс служит посредником между View и моделью данных
// Он позволяет открыть наш файл данных чтобы записывать и извлекать значения
@ -23,14 +24,21 @@ final class ClassProvider {
}
var newContext: NSManagedObjectContext {
persistentContainer.newBackgroundContext()
//persistentContainer.newBackgroundContext()
//Можно использовать объявление newContext с помощью строки, которая написана выше, но вариант ниже потокобезопаснее
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.persistentStoreCoordinator = persistentContainer.persistentStoreCoordinator
return context
}
private init() {
// Открытие файла
persistentContainer = NSPersistentContainer(name: "ClassDataModel")
if EnvironmentValues.isPreview {
persistentContainer.persistentStoreDescriptions.first?.url = .init(filePath: "/dev/null")
}
// Выставляем флаг для автоматического сохранения изменений данных из Veiw в память
// Выставляем флаг для автоматического слияния данных из фонового контекста в основной
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
// Выполняем открытие файла с данными
@ -39,6 +47,49 @@ final class ClassProvider {
fatalError("Unable to load store. Error: \(error)")
}
}
}
func exists(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) -> CoreDataClassModel? {
try? context.existingObject(with: lesson.objectID) as? CoreDataClassModel
}
func delete(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) throws {
if let existingClass = exists(lesson, in: context) {
context.delete(existingClass)
Task(priority: .background) {
try await context.perform {
try context.save()
}
}
}
}
func persist(in context: NSManagedObjectContext) throws {
if context.hasChanges {
try context.save()
}
}
}
extension EnvironmentValues {
static var isPreview: Bool {
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,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()
}
}
}
}

View File

@ -0,0 +1,101 @@
//
// FavGroupsView.swift
// Schedule ICTIS
//
// Created by G412 on 05.03.2025.
//
import SwiftUI
struct FavGroupsView: View {
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
var firstFavGroup = (UserDefaults.standard.string(forKey: "group") ?? "")
var secondFavGroup = (UserDefaults.standard.string(forKey: "group2") ?? "")
var thirdFavGroup = (UserDefaults.standard.string(forKey: "group3") ?? "")
var body: some View {
VStack (spacing: 0) {
List {
if firstFavGroup != "" {
HStack {
Text(firstFavGroup)
.font(.custom("Montserrat-Medium", fixedSize: 17))
Spacer()
}
.background(Color.white)
.cornerRadius(10)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
vm.removeFromSchedule(group: firstFavGroup)
UserDefaults.standard.set("", forKey: "group")
} label: {
Label("Удалить", systemImage: "trash")
}
}
}
if secondFavGroup != "" {
HStack {
Text(secondFavGroup)
.font(.custom("Montserrat-Medium", fixedSize: 17))
Spacer()
}
.background(Color.white)
.cornerRadius(10)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
vm.removeFromSchedule(group: secondFavGroup)
UserDefaults.standard.set("", forKey: "group2")
} label: {
Label("Удалить", systemImage: "trash")
}
}
}
if thirdFavGroup != "" {
HStack {
Text(thirdFavGroup)
.font(.custom("Montserrat-Medium", fixedSize: 17))
Spacer()
}
.background(Color.white)
.cornerRadius(10)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
vm.removeFromSchedule(group: thirdFavGroup)
UserDefaults.standard.set("", forKey: "group3")
} label: {
Label("Удалить", systemImage: "trash")
}
}
}
}
.frame(maxHeight: 400)
Spacer()
HStack {
Spacer()
if firstFavGroup == "" || secondFavGroup == "" || thirdFavGroup == "" {
NavigationLink(destination: SelectingGroupView(vm: vm, networkMonitor: networkMonitor, firstFavGroup: firstFavGroup, secondFavGroup: secondFavGroup, thirdFavGroup: thirdFavGroup)) {
HStack {
Image(systemName: "plus")
.foregroundColor(.white)
.font(.system(size: 22))
.padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12))
}
.background(Color("blueColor"))
.cornerRadius(10)
.padding(.trailing, 20)
}
}
}
.padding(.bottom, 90)
}
.background(Color("background"))
}
}
#Preview {
@Previewable @StateObject var vm = ScheduleViewModel()
@Previewable @StateObject var vm2 = NetworkMonitor()
FavGroupsView(vm: vm, networkMonitor: vm2)
}

View File

@ -0,0 +1,101 @@
//
// FavGroupsView.swift
// Schedule ICTIS
//
// Created by Egor Mironov on 05.03.2025.
//
import SwiftUI
struct FavVPKView: View {
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
var firstFavVPK = (UserDefaults.standard.string(forKey: "vpk1") ?? "")
var secondFavVPK = (UserDefaults.standard.string(forKey: "vpk2") ?? "")
var thirdFavVPK = (UserDefaults.standard.string(forKey: "vpk3") ?? "")
var body: some View {
VStack (spacing: 0) {
List {
if firstFavVPK != "" {
HStack {
Text(firstFavVPK)
.font(.custom("Montserrat-Medium", fixedSize: 17))
Spacer()
}
.background(Color.white)
.cornerRadius(10)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
vm.removeFromSchedule(group: firstFavVPK)
UserDefaults.standard.set("", forKey: "vpk1")
} label: {
Label("Удалить", systemImage: "trash")
}
}
}
if secondFavVPK != "" {
HStack {
Text(secondFavVPK)
.font(.custom("Montserrat-Medium", fixedSize: 17))
Spacer()
}
.background(Color.white)
.cornerRadius(10)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
vm.removeFromSchedule(group: secondFavVPK)
UserDefaults.standard.set("", forKey: "vpk2")
} label: {
Label("Удалить", systemImage: "trash")
}
}
}
if thirdFavVPK != "" {
HStack {
Text(thirdFavVPK)
.font(.custom("Montserrat-Medium", fixedSize: 17))
Spacer()
}
.background(Color.white)
.cornerRadius(10)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
vm.removeFromSchedule(group: thirdFavVPK)
UserDefaults.standard.set("", forKey: "vpk3")
} label: {
Label("Удалить", systemImage: "trash")
}
}
}
}
.frame(maxHeight: 400)
Spacer()
HStack {
Spacer()
if firstFavVPK == "" || secondFavVPK == "" || thirdFavVPK == "" {
NavigationLink(destination: SelectingVPKView(vm: vm, networkMonitor: networkMonitor, firstFavVPK: firstFavVPK, secondFavVPK: secondFavVPK, thirdFavVPK: thirdFavVPK)) {
HStack {
Image(systemName: "plus")
.foregroundColor(.white)
.font(.system(size: 22))
.padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12))
}
.background(Color("blueColor"))
.cornerRadius(10)
.padding(.trailing, 20)
}
}
}
.padding(.bottom, 90)
}
.background(Color("background"))
}
}
#Preview {
@Previewable @StateObject var vm = ScheduleViewModel()
@Previewable @StateObject var vm2 = NetworkMonitor()
FavVPKView(vm: vm, networkMonitor: vm2)
}

View File

@ -0,0 +1,91 @@
//
// GeneralGroupSettings.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 25.02.2025.
//
import SwiftUI
struct GeneralGroupSettings: View {
@Binding var selectedTheme: String
@Binding var selectedLanguage: String
var body: some View {
VStack {
HStack {
Text("Тема")
.font(.custom("Montserrat-Medium", fixedSize: 17))
.foregroundColor(.black)
Spacer()
HStack {
Text(selectedTheme)
.font(.custom("Montserrat-Medium", fixedSize: 17))
.foregroundColor(Color("customGray3"))
Image("upDownArrows")
.resizable()
.scaledToFit()
.frame(width: 15, height: 15)
}
.padding(.horizontal)
}
.padding(.horizontal)
.padding(.top, 17)
.padding(.bottom, 7)
.overlay {
HStack {
Spacer()
Picker("", selection: $selectedTheme, content: {
ForEach(MockData.themes, id: \.self) {
Text($0)
}
})
.padding(.trailing, 35)
.blendMode(.destinationOver)
}
.frame(width: UIScreen.main.bounds.width)
}
Rectangle()
.foregroundColor(Color("customGray1"))
.frame(height: 1)
.padding(.horizontal)
HStack {
Text("Язык")
.font(.custom("Montserrat-Medium", fixedSize: 17))
.foregroundColor(.black)
Spacer()
HStack {
Text(selectedLanguage)
.font(.custom("Montserrat-Medium", fixedSize: 17))
.foregroundColor(Color("customGray3"))
Image("upDownArrows")
.resizable()
.scaledToFit()
.frame(width: 15, height: 15)
}
.padding(.horizontal)
}
.padding(.horizontal)
.padding(.top, 7)
.padding(.bottom, 17)
.overlay {
HStack {
Spacer()
Picker("", selection: $selectedLanguage, content: {
ForEach(MockData.languages, id: \.self) {
Text($0)
}
})
.padding(.trailing, 35)
.blendMode(.destinationOver)
}
.frame(width: UIScreen.main.bounds.width)
}
}
.background(Color.white)
.cornerRadius(20)
}
}
#Preview {
GeneralGroupSettings(selectedTheme: .constant("Темная"), selectedLanguage: .constant("Русский"))
}

View File

@ -0,0 +1,55 @@
//
// 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

@ -0,0 +1,47 @@
//
// ScheduleGroupSettings.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 25.02.2025.
//
import SwiftUI
struct ScheduleGroupSettings: View {
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
var body: some View {
VStack {
NavigationLink(destination: FavGroupsView(vm: vm, networkMonitor: networkMonitor)) {
HStack {
Text("Избранное расписание")
.font(.custom("Montserrat-Medium", fixedSize: 17))
.foregroundColor(.black)
Spacer()
Image("arrowRight")
}
.padding(.horizontal)
.padding(.top, 12)
.padding(.bottom, 3)
}
Rectangle()
.foregroundColor(Color("customGray1"))
.frame(height: 1)
.padding(.horizontal)
NavigationLink(destination: FavVPKView(vm: vm, networkMonitor: networkMonitor)) {
HStack {
Text("ВПК")
.font(.custom("Montserrat-Medium", fixedSize: 17))
.foregroundColor(.black)
Spacer()
Image("arrowRight")
}
.padding(.horizontal)
.padding(.top, 3)
.padding(.bottom, 12)
}
}
.background(Color.white)
.cornerRadius(20)
}
}

View File

@ -0,0 +1,154 @@
//
// SelectedGroupView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 30.01.2025.
//
import SwiftUI
struct SelectingGroupView: View {
@Environment(\.dismiss) private var dismiss
@FocusState private var isFocused: Bool
@State private var text: String = ""
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
@State private var isLoading = false
@State private var searchTask: DispatchWorkItem?
@StateObject private var serchGroupsVM = SearchGroupsViewModel()
var firstFavGroup: String
var secondFavGroup: String
var thirdFavGroup: String
var body: some View {
VStack {
HStack (spacing: 0) {
Image(systemName: "magnifyingglass")
.foregroundColor(Color.gray)
.padding(.leading, 12)
.padding(.trailing, 7)
TextField("Поиск группы", text: $text)
.disableAutocorrection(true)
.focused($isFocused)
.onChange(of: text) { oldValue, newValue in
searchTask?.cancel()
let task = DispatchWorkItem {
if !text.isEmpty {
serchGroupsVM.fetchGroups(group: text)
}
else {
serchGroupsVM.fetchGroups(group: "кт")
}
}
searchTask = task
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: task)
}
.onSubmit {
self.isFocused = false
if (!text.isEmpty) {
vm.fetchWeekSchedule(isOtherWeek: false)
self.isLoading = true
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")
} else if secondFavGroup == "" {
UserDefaults.standard.set(text, forKey: "group2")
} else {
UserDefaults.standard.set(text, forKey: "group3")
}
vm.nameToHtml[text] = ""
vm.fetchWeekSchedule()
vm.updateFilteringGroups()
self.isLoading = false
self.text = ""
dismiss()
}
else {
vm.isShowingAlertForIncorrectGroup = true
vm.errorInNetwork = .invalidResponse
}
}
}
}
.submitLabel(.done)
if isFocused {
Button {
self.text = ""
self.isFocused = false
} label: {
Image(systemName: "xmark.circle.fill")
.padding(.trailing, 20)
.offset(x: 10)
.foregroundColor(.gray)
.background(
)
}
}
}
.frame(height: 40)
.background(
RoundedRectangle(cornerRadius: 15)
.fill(.white)
)
Spacer()
if isLoading {
LoadingView()
Spacer()
} else if networkMonitor.isConnected {
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 firstFavGroup == "" {
UserDefaults.standard.set(item.name, forKey: "group")
vm.nameToHtml[item.name] = ""
} else if secondFavGroup == "" {
UserDefaults.standard.set(item.name, forKey: "group2")
vm.nameToHtml[item.name] = ""
} else {
UserDefaults.standard.set(item.name, forKey: "group3")
vm.nameToHtml[item.name] = ""
}
vm.updateFilteringGroups()
vm.fetchWeekSchedule()
dismiss()
}
}
}
}
}
} else {
NetworkErrorView(message: "Восстановите подключение к интернету чтобы мы смогли загрузить список групп")
}
}
.padding(.horizontal, 10)
.background(Color("background"))
.onAppear {
serchGroupsVM.fetchGroups(group: "кт")
}
}
}
#Preview {
@Previewable @StateObject var vm = ScheduleViewModel()
@Previewable @StateObject var vm2 = NetworkMonitor()
SelectingGroupView(vm: vm, networkMonitor: vm2, firstFavGroup: "", secondFavGroup: "", thirdFavGroup: "")
}

View File

@ -0,0 +1,118 @@
//
// SelectedGroupView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 30.01.2025.
//
import SwiftUI
struct SelectingVPKView: View {
@Environment(\.dismiss) private var dismiss
@FocusState private var isFocused: Bool
@State private var text: String = ""
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
@State private var isLoading = false
@State private var searchTask: DispatchWorkItem?
@StateObject private var serchGroupsVM = SearchGroupsViewModel()
var firstFavVPK: String
var secondFavVPK: String
var thirdFavVPK: String
var body: some View {
VStack {
HStack (spacing: 0) {
Image(systemName: "magnifyingglass")
.foregroundColor(Color.gray)
.padding(.leading, 12)
.padding(.trailing, 7)
TextField("Поиск ВПК", text: $text)
.disableAutocorrection(true)
.focused($isFocused)
.onChange(of: text) { oldValue, newValue in
searchTask?.cancel()
let task = DispatchWorkItem {
if !text.isEmpty {
serchGroupsVM.fetchGroups(group: text)
}
else {
serchGroupsVM.fetchGroups(group: "ВПК")
}
}
searchTask = task
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: task)
}
.onSubmit {
self.isFocused = false
if (!text.isEmpty) {
vm.fetchWeekSchedule(isOtherWeek: false)
self.isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if vm.errorInNetwork == .noError {
vm.errorInNetwork = nil
if firstFavVPK == "" {
UserDefaults.standard.set(text, forKey: "vpk1")
} else if secondFavVPK == "" {
UserDefaults.standard.set(text, forKey: "vpk2")
} else {
UserDefaults.standard.set(text, forKey: "vpk3")
}
text = transformStringToFormat(text)
vm.nameToHtml[text] = ""
vm.updateFilteringGroups()
vm.fetchWeekSchedule()
self.isLoading = false
self.text = ""
print("✅ - Избранный ВПК был установлен")
dismiss()
}
else {
vm.isShowingAlertForIncorrectGroup = true
vm.errorInNetwork = .invalidResponse
}
}
}
}
.submitLabel(.done)
if isFocused {
Button {
self.text = ""
self.isFocused = false
} label: {
Image(systemName: "xmark.circle.fill")
.padding(.trailing, 20)
.offset(x: 10)
.foregroundColor(.gray)
.background(
)
}
}
}
.frame(height: 40)
.background(
RoundedRectangle(cornerRadius: 15)
.fill(.white)
)
Spacer()
if isLoading {
LoadingView()
Spacer()
} else if networkMonitor.isConnected {
ListOfGroupsView(vm: vm, serchGroupsVM: serchGroupsVM, firstFavVPK: firstFavVPK, secondFavVPK: secondFavVPK, thirdFavVPK: thirdFavVPK)
} else {
ConnectingToNetworkView()
}
}
.padding(.horizontal, 10)
.background(Color("background"))
.onAppear {
serchGroupsVM.fetchGroups(group: "ВПК")
}
}
}
#Preview {
@Previewable @StateObject var vm = ScheduleViewModel()
@Previewable @StateObject var vm2 = NetworkMonitor()
SelectingVPKView(vm: vm, networkMonitor: vm2, firstFavVPK: "", secondFavVPK: "", thirdFavVPK: "")
}

View File

@ -0,0 +1,48 @@
//
// SettingsView2.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 25.02.2025.
//
import SwiftUI
struct SettingsView: View {
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
@State private var selectedTheme = "Светлая"
@State private var selectedLanguage = "Русский"
var body: some View {
NavigationView {
VStack {
ScrollView (.vertical, showsIndicators: false) {
VStack (alignment: .leading) {
Text("Общие")
.font(.custom("Montserrat-Medium", fixedSize: 18))
.foregroundColor(Color("customGray3"))
.padding(.horizontal)
GeneralGroupSettings(selectedTheme: $selectedTheme, selectedLanguage: $selectedLanguage)
}
.padding(.top, 20)
VStack (alignment: .leading) {
Text("Расписание")
.font(.custom("Montserrat-Medium", fixedSize: 18))
.foregroundColor(Color("customGray3"))
.padding(.horizontal)
ScheduleGroupSettings(vm: vm, networkMonitor: networkMonitor)
}
.padding(.top, 20)
}
.padding(.horizontal)
}
.background(Color("background"))
.navigationTitle("Настройки")
}
}
}
#Preview {
@Previewable @StateObject var vm = ScheduleViewModel()
@Previewable @StateObject var vm2 = NetworkMonitor()
SettingsView(vm: vm, networkMonitor: vm2)
}

View File

@ -0,0 +1,28 @@
//
// TestingView.swift
// Schedule ICTIS
//
// Created by G412 on 05.03.2025.
//
import SwiftUI
struct TestingView: View {
var body: some View {
VStack {
Text("Hello")
Text("Hello")
Text("Hello")
Text("Hello")
Text("Hello")
Text("Hello")
Text("Hello")
Text("Hello")
Text("Hello")
}
}
}
#Preview {
TestingView()
}

View File

@ -2,14 +2,14 @@
// CustomTabBarView.swift
// Schedule ICTIS
//
// Created by Egor Mironov on 13.11.2024.
// Created by Mironov Egor on 13.11.2024.
//
import SwiftUI
struct TabBarView: View {
@Binding var selectedTab: TabBarModel
// @NameSpace private var animation
@Namespace private var animation
var body: some View {
VStack {
Spacer()
@ -20,12 +20,12 @@ struct TabBarView: View {
.padding(6)
.background(.white)
.mask(RoundedRectangle(cornerRadius: 24, style: .continuous))
.shadow(color: .black.opacity(0.2), radius: 8, x: 4, y: 4)
.shadow(color: .black.opacity(0.4), radius: 20, x: 8, y: 8)
//.background(
// background
// .shadow(.drop(color: .black.opacity(0.08), radius: 5, x: 5, y: 5))
// .shadow(.drop(color: .black.opacity(0.08), radius: 5, x: 5, y: -5)),
// .shadow(.drop(color: Color.black.opacity(0.08), radius: 5, x: 5, y: 5))
// .shadow(.drop(color: Color.black.opacity(0.08), radius: 5, x: 5, y: -5)),
// in: .capsule
//)
}
@ -40,6 +40,7 @@ struct TabBarView: View {
VStack (alignment: .center) {
Image(systemName: tab.rawValue)
.font(.title3)
.fontWeight(.regular)
}
.frame(width: 70, height: 28)
.foregroundStyle(selectedTab == tab ? Color.white : Color("blueColor"))
@ -49,8 +50,18 @@ struct TabBarView: View {
.background {
if selectedTab == tab {
Capsule()
.fill(Color("blueColor"))
// .matchedGeometryEffect(id: "ACTIVETAB", in: animation)
.fill(
LinearGradient(
gradient: Gradient(stops: [
.init(color: Color("blueColor").opacity(0.9), location: 0.0),
.init(color: Color("blueColor").opacity(0.9), location: 0.5),
.init(color: Color("blueColor").opacity(1.0), location: 1.0)
]),
startPoint: .top,
endPoint: .bottom
)
)
.matchedGeometryEffect(id: "ACTIVETAB", in: animation)
}
}
}
@ -58,7 +69,3 @@ struct TabBarView: View {
}
}
}
#Preview {
ContentView()
}

View File

@ -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)
}

View File

@ -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)
}
@ -71,8 +101,17 @@ extension View {
}
}
func hoursMinutesAreEqual(date1: Date, isEqualTo date2: Date) -> Bool {
let calendar = Calendar.current
let components1 = calendar.dateComponents([.day, .hour, .minute], from: date1)
let components2 = calendar.dateComponents([.day, .hour, .minute], from: date2)
return components1.day == components2.day && components1.hour == components2.hour && components1.minute == components2.minute
}
// MARK: ScheduleView
func datesAreEqual(_ date1: Date, _ date2: Date) -> Bool {
func daysAreEqual(_ date1: Date, _ date2: Date) -> Bool {
let calendar = Calendar.current
let components1 = calendar.dateComponents([.year, .month, .day], from: date1)
@ -135,7 +174,7 @@ extension View {
print("\(startHours) - \(endHours)")
print("\(startMinutes) - \(endMinutes)")
if startHours > endHours {
if Int(startHours) > Int(endHours) {
return false
}
else if startHours == endHours {
@ -150,11 +189,161 @@ extension View {
return true
}
}
}
func checkUpFields(_ subject: String, _ auditory: String, _ professor: String, _ time1: Date, _ time3: Date) -> Bool {
if (subject != "" || auditory != "" || professor != "") {
return true
extension WeekTabView {
func updateWeekScreenViewForNewGroup() {
vm.updateSelectedDayIndex()
if weekSlider.isEmpty {
let currentWeek = Date().fetchWeek(vm.selectedDay)
if let firstDate = currentWeek.first?.date {
weekSlider.append(firstDate.createPrevioustWeek())
}
weekSlider.append(currentWeek)
if let lastDate = currentWeek.last?.date {
weekSlider.append(lastDate.createNextWeek())
}
}
}
}
extension WeekViewForWeek {
func paginateWeek() {
let calendar = Calendar.current
let groupsKeys = Array (vm.nameToHtml.keys)
if weekSlider.indices.contains(currentWeekIndex) {
if let firstDate = weekSlider[currentWeekIndex].first?.date,
currentWeekIndex == 0 {
vm.week -= 1
if !groupsKeys.isEmpty {
vm.fetchWeekSchedule(isOtherWeek: true)
}
if UserDefaults.standard.string(forKey: "vpk") != nil {
//vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk"))
}
weekSlider.insert(firstDate.createPrevioustWeek(), at: 0)
weekSlider.removeLast()
currentWeekIndex = 1
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -1, to: vm.selectedDay) ?? Date.init()
vm.updateSelectedDayIndex()
}
if let lastDate = weekSlider[currentWeekIndex].last?.date,
currentWeekIndex == (weekSlider.count - 1) {
vm.week += 1
if !groupsKeys.isEmpty {
vm.fetchWeekSchedule(isOtherWeek: true)
}
weekSlider.append(lastDate.createNextWeek())
weekSlider.removeFirst()
currentWeekIndex = weekSlider.count - 2
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 1, to: vm.selectedDay) ?? Date.init()
vm.updateSelectedDayIndex()
}
}
}
}
extension WeekViewForMonth {
func getForegroundColor(day: Date.WeekDay) -> Color {
if isDateInCurrentMonth(day.date) {
return isSameDate(day.date, vm.selectedDay) ? .white : .black
} else {
return isSameDate(day.date, vm.selectedDay) ? .white : Color("greyForDaysInMonthTabView")
}
}
func getBackgroundColor(day: Date.WeekDay) -> Color {
return isSameDate(day.date, vm.selectedDay) ? Color("blueColor") : Color("background")
}
func overlay(day: Date.WeekDay) -> some View {
Group {
if day.date.isToday && !isSameDate(day.date, vm.selectedDay) {
RoundedRectangle(cornerRadius: 100)
.stroke(Color("blueColor"), lineWidth: 2)
}
}
}
func handleTap(day: Date.WeekDay) {
if isSameWeek(day.date, vm.selectedDay) {
print("На одной неделе")
}
else {
let groupsKeys = Array(vm.nameToHtml.keys)
var difBetweenWeeks = weeksBetween(startDate: vm.selectedDay, endDate: day.date)
if day.date < vm.selectedDay {
difBetweenWeeks = difBetweenWeeks * -1
}
print(difBetweenWeeks)
vm.week += difBetweenWeeks
if !groupsKeys.isEmpty {
vm.fetchWeekSchedule(isOtherWeek: true)
}
if UserDefaults.standard.string(forKey: "vpk") != nil {
//vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk"))
}
}
vm.selectedDay = day.date
vm.updateSelectedDayIndex()
}
}
extension MonthTabView {
func updateMonthScreenViewForNewGroup() {
vm.updateSelectedDayIndex()
if monthSlider.isEmpty {
let currentMonth = Date().fetchMonth(vm.selectedDay)
if let firstDate = currentMonth.first?.week[0].date {
let temp = firstDate.createPreviousMonth()
print("First date - \(firstDate)")
print(temp)
monthSlider.append(temp)
}
monthSlider.append(currentMonth)
if let lastDate = currentMonth.last?.week[6].date {
let temp = lastDate.createNextMonth()
monthSlider.append(temp)
}
}
}
func paginateMonth(_ indexOfWeek: Int = 0) {
let calendar = Calendar.current
let groupsKeys = Array (vm.nameToHtml.keys)
if monthSlider.indices.contains(currentMonthIndex) {
if let firstDate = monthSlider[currentMonthIndex].first?.week[0].date,
currentMonthIndex == 0 {
monthSlider.insert(firstDate.createPreviousMonth(), at: 0)
monthSlider.removeLast()
currentMonthIndex = 1
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -5, to: vm.selectedDay) ?? Date.init()
vm.updateSelectedDayIndex()
vm.week -= 5
if !groupsKeys.isEmpty {
vm.fetchWeekSchedule(isOtherWeek: true)
}
}
if let lastDate = monthSlider[currentMonthIndex].last?.week[6].date,
currentMonthIndex == (monthSlider.count - 1) {
monthSlider.append(lastDate.createNextMonth())
monthSlider.removeFirst()
currentMonthIndex = monthSlider.count - 2
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 5, to: vm.selectedDay) ?? Date.init()
vm.updateSelectedDayIndex()
vm.week += 5
if !groupsKeys.isEmpty {
vm.fetchWeekSchedule(isOtherWeek: true)
}
}
}
return true
}
}

View File

@ -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:
"Ошибки нет"
}

View File

@ -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 {
@ -57,4 +62,18 @@ final class NetworkManager {
throw NetworkError.invalidData
}
}
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 customSession.data(from: url)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse }
do {
return try decoder.decode(Welcome.self, from: data)
}
catch {
throw NetworkError.invalidData
}
}
}

View File

@ -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()
}
}

View File

@ -2,25 +2,37 @@
// 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
private let provider: ClassProvider
private let context: NSManagedObjectContext
init(provider: ClassProvider, _class: ClassModel? = nil) {
init(provider: ClassProvider, _class: CoreDataClassModel? = nil) {
self.provider = provider
self.context = provider.newContext
self._class = ClassModel(context: self.context)
if let _class,
let existingClassCopy = provider.exists(_class, in: context) {
self._class = existingClassCopy
self.isNew = false
}
else {
self._class = CoreDataClassModel(context: self.context)
self.isNew = true
}
}
func save() throws {
if context.hasChanges {
try context.save()
}
try provider.persist(in: context)
}
}

View File

@ -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)
}
}

View File

@ -6,11 +6,19 @@
//
import Foundation
import SwiftUICore
@MainActor
final class ScheduleViewModel: ObservableObject {
//MARK: Properties
@Published var weekSchedule: Table = Table(
@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(
type: "",
name: "",
week: 0,
@ -19,53 +27,89 @@ final class ScheduleViewModel: ObservableObject {
link: ""
)
@Published var selectedDay: Date = .init()
@Published var selectedIndex: Int = 1
@Published var classes: [[String]] = []
@Published var selectedIndex: Int = 0
@Published var week: Int = 0
@Published var numOfGroup: String = ""
@Published var isFirstStartOffApp = true
@Published var isShowingAlertForIncorrectGroup: Bool = false
@Published var errorInNetwork: NetworkError?
@Published var isLoading: Bool = false
@Published var group: String = ""
@Published var isNewGroup: Bool = false
//MARK: Methods
func fetchWeekSchedule(_ group: String, _ num: Int = 0) {
func fetchWeekSchedule(isOtherWeek: Bool = false) {
isLoading = true
Task {
do {
var schedule: Schedule
if (num != 0) {
week += num
schedule = try await NetworkManager.shared.getScheduleForOtherWeek(week, numOfGroup)
var updatedClassesGroups: [[ClassInfo]] = Array(repeating: [], count: 6) // 6 дней (пн-сб)
// Если другая неделя, запрашиваем расписание по неделе и номеру группу(в HTML формате)
if isOtherWeek {
let groupHTMLs = Array(self.nameToHtml.values)
for groupHTML in groupHTMLs {
let schedule = try await NetworkManager.shared.getScheduleForOtherWeek(self.week, groupHTML)
let table = schedule.table.table
let nameOfGroup = schedule.table.name
// Преобразуем данные в формат ClassInfo
for (dayIndex, day) in table[2...].enumerated() { // Пропускаем первые две строки (заголовки)
for (timeIndex, subject) in day.enumerated() {
if !subject.isEmpty && timeIndex > 0 { // Пропускаем первый столбец (день и дату)
let time = table[1][timeIndex] // Время берем из второй строки
let classInfo = ClassInfo(subject: subject, group: nameOfGroup, time: time)
updatedClassesGroups[dayIndex].append(classInfo)
}
else {
schedule = try await NetworkManager.shared.getSchedule(group)
}
self.weekSchedule = schedule.table
self.week = weekSchedule.week
self.numOfGroup = weekSchedule.group
self.classes = weekSchedule.table
}
}
} else {
let groupNames = Array(self.nameToHtml.keys)
for groupName in groupNames {
let schedule = try await NetworkManager.shared.getSchedule(groupName)
let numberHTML = schedule.table.group
self.nameToHtml[groupName] = numberHTML
let table = schedule.table.table
self.week = schedule.table.week
// Преобразуем данные в формат ClassInfo
for (dayIndex, day) in table[2...].enumerated() { // Пропускаем первые две строки (заголовки)
for (timeIndex, subject) in day.enumerated() {
if !subject.isEmpty && timeIndex > 0 { // Пропускаем первый столбец (день и дату)
let time = table[1][timeIndex] // Время берем из второй строки
let classInfo = ClassInfo(subject: subject, group: groupName, time: time)
updatedClassesGroups[dayIndex].append(classInfo)
}
}
}
}
}
// Обновляем данные
self.classesGroups = updatedClassesGroups
self.isFirstStartOffApp = false
self.isShowingAlertForIncorrectGroup = false
self.isLoading = false
self.errorInNetwork = .noError
}
catch {
if let error = error as? NetworkError {
switch (error) {
// Сортируем по времени
self.sortClassesByTime()
} catch {
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
case .invalidData:
errorInNetwork = .invalidData
self.isShowingAlertForIncorrectGroup = true
default:
print(2)
print("Неизвестная ошибка: \(error)")
}
print("Есть ошибка: \(error)")
}
isLoading = false
print(error)
}
}
}
}
@ -73,20 +117,97 @@ final class ScheduleViewModel: ObservableObject {
func updateSelectedDayIndex() {
switch selectedDay.format("E") {
case "Пн":
selectedIndex = 2
selectedIndex = 0
case "Вт":
selectedIndex = 3
selectedIndex = 1
case "Ср":
selectedIndex = 4
selectedIndex = 2
case "Чт":
selectedIndex = 5
selectedIndex = 3
case "Пт":
selectedIndex = 6
selectedIndex = 4
case "Сб":
selectedIndex = 7
selectedIndex = 5
default:
selectedIndex = 8
selectedIndex = 6
}
}
private func parseTime(_ timeString: String) -> Int {
// Разделяем строку по дефису и берем первую часть (время начала)
let startTimeString = timeString.components(separatedBy: "-").first ?? ""
// Разделяем время на часы и минуты
let components = startTimeString.components(separatedBy: ":")
guard components.count == 2,
let hours = Int(components[0]),
let minutes = Int(components[1]) else {
return 0 // В случае ошибки возвращаем 0
}
// Преобразуем время в минуты с начала дня
return hours * 60 + minutes
}
// Method for sorting classes by time
private func sortClassesByTime() {
// Проходим по каждому дню (подмассиву) в classesGroups
for dayIndex in 0..<classesGroups.count {
// Сортируем подмассив по времени начала пары
classesGroups[dayIndex].sort { class1, class2 in
let time1 = parseTime(class1.time) // Время начала первой пары
let time2 = parseTime(class2.time) // Время начала второй пары
return time1 < time2 // Сортируем по возрастанию
}
}
}
func removeFromSchedule(group: String) {
self.nameToHtml[group] = nil
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 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] = ""
}
}
}

View File

@ -0,0 +1,137 @@
//
// SearchGroupsViewModel.swift
// Schedule ICTIS
//
// Created by G412 on 06.03.2025.
//
import Foundation
@MainActor
final class SearchGroupsViewModel: ObservableObject {
@Published var groups: [Subject] = []
func fetchGroups(group: String) {
Task {
do {
var groups: Welcome
groups = try await NetworkManager.shared.getGroups(group: group)
self.groups = groups.choices
if (group == "кт") {
self.sortGroups()
} else {
self.sortVPK()
}
}
catch {
if let error = error as? NetworkError {
switch (error) {
case .invalidData:
self.groups.removeAll()
default:
self.groups.removeAll()
print("Неизвестная ошибка: \(error)")
}
print("Есть ошибка: \(error)")
}
}
}
}
// Метод сортировки
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)
}
}

38
Schedule-ICTIS-Info.plist Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIAppFonts</key>
<array>
<string>Montserrat-Black.ttf</string>
<string>Montserrat-BlackItalic.ttf</string>
<string>Montserrat-Bold.ttf</string>
<string>Montserrat-BoldItalic.ttf</string>
<string>Montserrat-ExtraBold.ttf</string>
<string>Montserrat-ExtraBoldItalic.ttf</string>
<string>Montserrat-ExtraLight.ttf</string>
<string>Montserrat-ExtraLightItalic.ttf</string>
<string>Montserrat-Italic.ttf</string>
<string>Montserrat-Light.ttf</string>
<string>Montserrat-LightItalic.ttf</string>
<string>Montserrat-Medium.ttf</string>
<string>Montserrat-MediumItalic.ttf</string>
<string>Montserrat-Regular.ttf</string>
<string>Montserrat-SemiBold.ttf</string>
<string>Montserrat-SemiBoldItalic.ttf</string>
<string>Montserrat-Thin.ttf</string>
<string>Montserrat-ThinItalic.ttf</string>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string></string>
<key>UTTypeTagSpecification</key>
<dict/>
</dict>
</array>
</dict>
</plist>