Compare commits

...

21 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
8d973e7942 Commit 2024-12-20 13:27:03 +03:00
1eb574e682 Lots of changes 2024-12-20 13:26:50 +03:00
112 changed files with 3490 additions and 928 deletions

View File

@ -0,0 +1,22 @@
<?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="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"/>
<attribute name="endtime" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="important" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="notification" optional="YES" attributeType="String"/>
<attribute name="online" optional="YES" attributeType="String"/>
<attribute name="professor" optional="YES" attributeType="String"/>
<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

@ -3,4 +3,22 @@
uuid = "38CE0E1B-29C0-4785-BF18-FE1BA38F677F"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "CF2C0E34-74B0-458B-AE66-E61DEB75A958"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Schedule ICTIS/Main/Views/ProfessorAuditoryClassFieldView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "20"
endingLineNumber = "20"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 B

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 = ViewModel()
@State private var selectedTab: TabBarModel = .schedule
@State private var isTabBarHidden = false
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
var body: some View {
TabView(selection: $selectedTab) {
Text("Tasks")
.tabItem {
Image(systemName: "books.vertical")
Text("Задания")
}
.tag(0)
MainView(vm: vm)
.tabItem {
Image(systemName: "house")
Text("Расписание")
}
.tag(1)
Text("Settings")
.tabItem {
Image(systemName: "gear")
Text("Настройки")
}
.tag(2)
ZStack (alignment: .bottom) {
TabView(selection: $selectedTab) {
Text("Tasks")
.tag(TabBarModel.tasks)
MainView(vm: vm, networkMonitor: networkMonitor)
.tag(TabBarModel.schedule)
.background {
if !isTabBarHidden {
HideTabBar {
print("TabBar is hidden")
isTabBarHidden = true
}
}
}
SettingsView(vm: vm, networkMonitor: networkMonitor)
.tag(TabBarModel.settings)
}
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

@ -1,183 +0,0 @@
//
// MonthTabView.swift
// Schedule ICTIS
//
// Created by G412 on 10.12.2024.
//
import SwiftUI
struct MonthTabView: View {
@State private var currentMonthIndex: Int = 1
@State private var monthSlider: [[Date.MonthWeek]] = []
@State private var createMonth: Bool = false
@State private var currentWeekIndex: Int = 0
@ObservedObject var vm: ViewModel
var body: some View {
VStack {
HStack (spacing: 34) {
ForEach(MockData.daysOfWeek.indices, id: \.self) { index in
Text(MockData.daysOfWeek[index])
.font(.system(size: 15, weight: .semibold))
.foregroundColor(MockData.daysOfWeek[index] == "Вс" ? Color(.red) : Color("customGray2"))
.padding(.top, 13)
.foregroundColor(.gray)
}
}
.padding(.top, 14)
//.background(Color.red)
TabView(selection: $currentMonthIndex) {
ForEach(monthSlider.indices, id: \.self) { index in
let month = monthSlider[index]
MonthView(month)
.tag(index)
}
}
.padding(.top, -25)
.padding(.bottom, -10)
.padding(.horizontal, -15)
.tabViewStyle(.page(indexDisplayMode: .never))
//.background(Color.green)
}
.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())
}
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
}
}
}
@ViewBuilder
func MonthView(_ month: [Date.MonthWeek]) -> some View {
VStack (spacing: 10) {
ForEach(month.indices, id: \.self) { index in
let week = month[index].week
WeekView(week)
}
}
.background {
GeometryReader {
let minX = $0.frame(in: .global).minX
Color.clear
.preference(key: OffsetKey.self, value: minX)
.onPreferenceChange(OffsetKey.self) { value in
if (abs(value.rounded()) - 20) < 5 && createMonth {
paginateMonth()
createMonth = false
}
}
}
}
}
@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

@ -1,152 +0,0 @@
//
// WeekTabView.swift
// Schedule ICTIS
//
// Created by G412 on 10.12.2024.
//
import SwiftUI
struct WeekTabView: View {
@State private var currentWeekIndex: Int = 1
@State private var weekSlider: [[Date.WeekDay]] = []
@State private var createWeek: Bool = false
@ObservedObject var vm: ViewModel
var body: some View {
HStack {
TabView(selection: $currentWeekIndex) {
ForEach(weekSlider.indices, id: \.self) { index in
let week = weekSlider[index]
WeekView(week)
.padding(.horizontal, 15)
.tag(index)
}
}
.padding(.horizontal, -15)
.tabViewStyle(.page(indexDisplayMode: .never))
.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())
}
}
})
.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()
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()
}
}
}
}
#Preview {
ContentView()
}

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

@ -7,13 +7,14 @@
import SwiftUI
struct CommentView: View {
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)
@ -41,6 +42,3 @@ struct CommentView: View {
}
}
#Preview {
SheetCreateClassView(isShowingSheet: .constant(true))
}

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 FieldView: 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 FieldView: View {
}
#Preview {
ContentView()
ProfessorFieldView(text: .constant(""), labelForField: "Преподаватель")
}

View File

@ -0,0 +1,61 @@
//
// StartEndTimeView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 17.12.2024.
//
import SwiftUI
struct StartEndTimeFieldView: View {
@Binding var isIncorrectDate: Bool
@Binding var selectedDay: Date
@Binding var selectedTime: Date
var imageName: String
var text: String
@Binding var isTimeSelected: Bool
var body: some View {
HStack {
Image(systemName: imageName)
.foregroundColor(isIncorrectDate ? .red : Color("grayForFields"))
.padding(.leading, 12)
.padding(.trailing, 5)
if !isTimeSelected || isIncorrectDate {
Text(text)
.font(.custom("Montserrat-Meduim", fixedSize: 17))
.foregroundColor(.gray.opacity(0.5))
}
else {
Text("\(selectedTime, formatter: timeFormatter)")
.foregroundColor(isIncorrectDate ? .red : .black)
.font(.custom("Montserrat-Medium", fixedSize: 17))
.padding(.trailing, 10)
}
Spacer()
}
.frame(width: (UIScreen.main.bounds.width / 2) - 22, height: 40)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.overlay {
if selectedDay.isToday {
DatePicker("", selection: $selectedTime, in: Date()..., displayedComponents: .hourAndMinute)
.padding(.trailing, 35)
.blendMode(.destinationOver)
.onChange(of: selectedTime) { newValue, oldValue in
isTimeSelected = true
}
}
else {
DatePicker("", selection: $selectedTime, displayedComponents: .hourAndMinute)
.padding(.trailing, 35)
.blendMode(.destinationOver)
.onChange(of: selectedTime) { newValue, oldValue in
isTimeSelected = true
}
}
}
}
}

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.2)
}
}
}
#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: ViewModel
@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)
}
else if (vm.isFirstStartOffApp) {
FirstLaunchScheduleView()
SearchBarView(text: $searchText, isFocused: _isFocusedSearchBar, vm: vm, isShowingMonthSlider: $isShowingMonthSlider)
.onChange(of: isScrolling, initial: false) { oldValue, newValue in
if newValue && isScrolling {
isFocusedSearchBar = false
}
}
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,12 +66,12 @@ 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()
.scaledToFit()
.frame(width: 15, height: 15) // Установите размер изображения
.frame(width: 15, height: 15)
}
}
}
@ -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,84 +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: ViewModel
var body: some View {
if vm.isLoading {
LoadingView(isLoading: $vm.isLoading)
@ObservedObject var vm: ScheduleViewModel
@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
}
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))
}
.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()
}
.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)
}
}
private var hasClassesToShow: Bool {
classes.contains { _class in
_class.day == vm.selectedDay
}
}
var body: some View {
ZStack(alignment: .top) {
if networkMonitor.isConnected {
onlineContent
} else {
offlineContent
}
else {
gradientOverlay
}
.onAppear {
deleteClassesFormCoreDataIfMonday()
if networkMonitor.isConnected {
checkSavingOncePerDay()
}
}
.sheet(item: $selectedClass, onDismiss: { selectedClass = nil }) { _class in
CreateEditClassView(vm: .init(provider: provider, _class: _class), day: vm.selectedDay)
}
}
// Онлайн-контент (с интернетом)
private var onlineContent: some View {
Group {
if vm.errorInNetwork == .timeout {
NetworkErrorView(message: "Проверьте подключение к интернету")
} else if vm.isLoading {
LoadingScheduleView()
} else if vm.errorInNetwork != .invalidResponse {
scheduleScrollView(isOnline: true)
} else {
NoScheduleView()
}
}
.onAppear {
if vm.classesGroups.isEmpty {
vm.fetchWeekSchedule()
}
}
}
// Оффлайн-контент (без интернета)
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")
}
}
}
#Preview {
ContentView()
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,12 @@ 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: ViewModel
@ObservedObject var vm: ScheduleViewModel
@Binding var isShowingMonthSlider: Bool
var provider = ClassProvider.shared
var body: some View {
HStack (spacing: 11) {
@ -22,40 +25,44 @@ 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) {
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 {
Button {
self.text = ""
self.isEditing = false
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
} label: {
Image(systemName: "xmark.circle.fill")
.padding(.trailing, 20)
.offset(x: 10)
.foregroundColor(.gray)
.background(
)
if isFocused {
Button {
self.text = ""
self.isFocused = false
} label: {
Image(systemName: "xmark.circle.fill")
.padding(.trailing, 20)
.offset(x: 10)
.foregroundColor(.gray)
.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: {
@ -78,11 +85,8 @@ struct SearchBarView: View {
.frame(height: 40)
.accentColor(.blue)
.sheet(isPresented: $isShowingSheet) {
SheetCreateClassView(isShowingSheet: $isShowingSheet)
CreateEditClassView(vm: .init(provider: provider), day: vm.selectedDay)
}
}
}
#Preview {
ContentView()
}

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,123 +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 selectedDay: Date = Date()
@State private var selectedStartTime: Date = Date()
@State private var selectedEndTime: Date = Date()
@State private var isImportant: Bool = false
@State private var selectedOption: String = "Нет"
@State private var textForComment: String = ""
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack {
FieldView(text: $textForNameOfClass, nameOfImage: "book", labelForField: "Предмет")
.padding(.bottom, 10)
FieldView(text: $textForNameOfAuditory, nameOfImage: "mappin.and.ellipse", labelForField: "Корпус-аудитория")
.padding(.bottom, 10)
FieldView(text: $textForNameOfProfessor, 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("\(selectedDay, 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: $selectedDay, in: Date()..., displayedComponents: .date)
.blendMode(.destinationOver)
}
.padding(.bottom, 10)
HStack {
StartEndTimeView(selectedTime: $selectedStartTime, imageName: "clock", text: "Начало")
Spacer()
StartEndTimeView(selectedTime: $selectedEndTime, imageName: "clock.badge.xmark", text: "Конец")
}
.frame(height: 40)
.padding(.bottom, 10)
Toggle("Пометить как важную", isOn: $isImportant)
.frame(height: 40)
.padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.padding(.bottom, 10)
HStack {
Text("Напоминанние")
Spacer()
Picker("Напоминание", selection: $selectedOption, 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)
CommentView(textForComment: $textForComment)
Spacer()
}
.padding(.horizontal)
.padding(.bottom, 60)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Отменить") {
isShowingSheet = false
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Сохранить") {
isShowingSheet = false
}
}
}
.navigationTitle("Новая пара")
.background(Color("background"))
}
}
private var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}
}
#Preview {
SheetCreateClassView(isShowingSheet: .constant(true))
}

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,59 +0,0 @@
//
// StartEndTimeView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 17.12.2024.
//
import SwiftUI
struct StartEndTimeView: View {
@Binding var selectedTime: Date
var imageName: String
var text: String
@State private var isTimeSelected: Bool = false
var body: some View {
HStack {
Image(systemName: imageName)
.foregroundColor(Color("grayForFields"))
.padding(.leading, 12)
if !isTimeSelected {
Text(text)
.font(.system(size: 17, weight: .regular))
.foregroundColor(.gray.opacity(0.5))
}
if isTimeSelected {
Text("\(selectedTime, formatter: timeFormatter)")
.foregroundColor(.black)
.font(.system(size: 17, weight: .medium))
.padding(.trailing, 10)
}
Spacer()
}
.frame(width: (UIScreen.main.bounds.width / 2) - 22, height: 40)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
.overlay {
DatePicker("", selection: $selectedTime, in: Date()..., displayedComponents: .hourAndMinute)
.padding(.trailing, 35)
.blendMode(.destinationOver)
.onChange(of: selectedTime) { newValue, oldValue in
isTimeSelected = true
}
}
}
private var timeFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
return formatter
}
}
#Preview {
StartEndTimeView(selectedTime: .constant(Date()), imageName: "clock", text: "Начало")
}

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

@ -0,0 +1,84 @@
//
// MonthTabView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 10.12.2024.
//
import SwiftUI
struct MonthTabView: View {
@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: 33) {
ForEach(MockData.daysOfWeek.indices, id: \.self) { index in
Text(MockData.daysOfWeek[index])
.font(.custom("Montserrat-SemiBold", fixedSize: 15))
.foregroundColor(MockData.daysOfWeek[index] == "Вс" ? Color(.red) : Color("customGray2"))
}
}
TabView(selection: $currentMonthIndex) {
ForEach(monthSlider.indices, id: \.self) { index in
let month = monthSlider[index]
MonthView(month)
.tag(index)
.transition(.slide)
}
}
.padding(.horizontal, -15)
.tabViewStyle(.page(indexDisplayMode: .never))
//.animation(.easeIn(duration: 0.3), value: currentMonthIndex)
}
.frame(height: 220)
.padding(.top, 26)
.padding(.bottom, 20)
.onAppear {
updateMonthScreenViewForNewGroup()
}
.onChange(of: currentMonthIndex, initial: false) { oldValue, newValue in
if newValue == 0 || newValue == (monthSlider.count - 1) {
createMonth = true
}
}
.onChange(of: vm.isNewGroup, initial: false) { oldValue, newValue in
if newValue {
monthSlider.removeAll()
currentMonthIndex = 1
updateMonthScreenViewForNewGroup()
vm.isNewGroup = false
}
}
}
@ViewBuilder
func MonthView(_ month: [Date.MonthWeek]) -> some View {
VStack (spacing: 10) {
ForEach(month.indices, id: \.self) { index in
let week = month[index].week
WeekViewForMonth(week: week, vm: vm)
}
}
.background {
GeometryReader {
let minX = $0.frame(in: .global).minX
Color.clear
.preference(key: OffsetKey.self, value: minX)
.onPreferenceChange(OffsetKey.self) { value in
if (abs(value.rounded()) - 20) < 5 && createMonth {
paginateMonth()
createMonth = false
}
}
}
}
}
}

View File

@ -0,0 +1,46 @@
//
// WeekTabView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 10.12.2024.
//
import SwiftUI
struct WeekTabView: View {
@State private var currentWeekIndex: Int = 1
@State var weekSlider: [[Date.WeekDay]] = []
@State private var createWeek: Bool = false
@ObservedObject var vm: ScheduleViewModel
var body: some View {
HStack {
TabView(selection: $currentWeekIndex) {
ForEach(weekSlider.indices, id: \.self) { index in
let week = weekSlider[index]
WeekViewForWeek(weekSlider: $weekSlider, currentWeekIndex: $currentWeekIndex, createWeek: $createWeek, week: week, vm: vm)
.padding(.horizontal, 15)
.tag(index)
}
}
.padding(.horizontal, -15)
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(height: 90)
}
.onAppear(perform: {
updateWeekScreenViewForNewGroup()
})
.onChange(of: currentWeekIndex, initial: false) { oldValue, newValue in
if newValue == 0 || newValue == (weekSlider.count - 1) {
createWeek = true
}
}
.onChange(of: vm.isNewGroup, initial: false) { oldValue, newValue in
if newValue {
weekSlider.removeAll()
currentWeekIndex = 1
updateWeekScreenViewForNewGroup()
vm.isNewGroup = false
}
}
}
}

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

@ -1,58 +0,0 @@
//
// TextFiledView.swift
// Schedule ICTIS
//
// Created by G412 on 17.12.2024.
//
import SwiftUI
struct TextFiledView: View {
@State private var isEditing: Bool = false
@State private var text: String = ""
@State private var nameOfImage: String = "calendar"
@State private var labelForField: String = "Преподаватель"
@FocusState private var isTextFieldFocused: Bool
var body: some View {
HStack(spacing: 0) {
Image(systemName: nameOfImage)
.foregroundColor(Color.gray)
.padding(.leading, 12)
.padding(.trailing, 7)
TextField(labelForField, text: $text)
.font(.system(size: 18, weight: .regular))
.disableAutocorrection(true)
.focused($isTextFieldFocused)
.onChange(of: isTextFieldFocused) { newValue, oldValue in
isEditing = newValue
}
.submitLabel(.done)
if isTextFieldFocused {
Button {
self.text = ""
self.isEditing = false
isTextFieldFocused = false
} label: {
Image(systemName: "xmark.circle.fill")
.padding()
.padding(.trailing, 20)
.offset(x: 10)
.foregroundColor(.red)
}
}
}
.frame(height: 40)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
}
}
#Preview {
TextFiledView()
}

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
@ -10,5 +10,15 @@ import Foundation
struct MockData {
static let daysOfWeek = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
// MARK: SheetCreateClassView
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

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

@ -2,13 +2,13 @@
// Tab.swift
// Schedule ICTIS
//
// Created by G412 on 13.11.2024.
// Created by Mironov Egor on 13.11.2024.
//
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" : "Vector.png",
"filename" : "arrowdown.svg",
"idiom" : "universal",
"scale" : "1x"
},

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" : "arrow.png",
"filename" : "arrowup.svg",
"idiom" : "universal",
"scale" : "1x"
},

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,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

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

@ -0,0 +1,95 @@
//
// ClassProvider.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 18.12.2024.
//
import Foundation
import CoreData
import SwiftUI
// Это класс служит посредником между View и моделью данных
// Он позволяет открыть наш файл данных чтобы записывать и извлекать значения
// Объект этого класса должен быть единственным за весь жизненный цикл приложения, чтобы не было рассинхронизации
// Для этого мы делаем его синглтоном
final class ClassProvider {
static let shared = ClassProvider()
// Это свойство для хранения открытого файла модели данных
private let persistentContainer: NSPersistentContainer
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
var newContext: NSManagedObjectContext {
//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")
}
// Выставляем флаг для автоматического слияния данных из фонового контекста в основной
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
// Выполняем открытие файла с данными
persistentContainer.loadPersistentStores {_, error in
if let error {
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,9 +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()
}

Some files were not shown because too many files have changed in this diff Show More