Compare commits

...

27 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
3fad13097a Commit 2024-12-17 15:02:13 +03:00
def9175c20 Commit 2024-12-12 14:59:35 +03:00
92b125927d Commit 2024-12-12 13:37:07 +03:00
1de531abc8 Commit 2024-12-11 13:33:35 +03:00
57e241292f Commit 2024-12-10 14:11:10 +03:00
f59d00016b Commit 2024-12-09 12:55:01 +03:00
110 changed files with 3943 additions and 500 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

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
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

@ -9,22 +9,69 @@ import SwiftUI
struct ContentView: View {
@State private var selectedTab: TabBarModel = .schedule
@StateObject var vm = ViewModel()
@State private var isTabBarHidden = false
@ObservedObject var vm: ScheduleViewModel
@ObservedObject var networkMonitor: NetworkMonitor
var body: some View {
ZStack {
switch selectedTab {
case .schedule:
MainView(vm: vm)
case .tasks:
ZStack (alignment: .bottom) {
TabView(selection: $selectedTab) {
Text("Tasks")
case .settings:
Text("Settings")
}
TabBarView(selectedTab: $selectedTab)
.tag(TabBarModel.tasks)
MainView(vm: vm, networkMonitor: networkMonitor)
.tag(TabBarModel.schedule)
.background {
if !isTabBarHidden {
HideTabBar {
print("TabBar is hidden")
isTabBarHidden = true
}
}
}
#Preview {
ContentView()
SettingsView(vm: vm, networkMonitor: networkMonitor)
.tag(TabBarModel.settings)
}
TabBarView(selectedTab: $selectedTab)
}
.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 {
@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

@ -1,14 +0,0 @@
//
// View+Extensions.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 15.11.2024.
//
import SwiftUI
extension View {
func isSameDate(_ date1: Date, _ date2: Date) -> Bool {
return Calendar.current.isDate(date1, inSameDayAs: date2)
}
}

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,28 +0,0 @@
//
// FirstLaunchScheduleView.swift
// Schedule ICTIS
//
// Created by G412 on 06.12.2024.
//
import SwiftUI
struct FirstLaunchScheduleView: View {
var body: some View {
VStack (alignment: .center) {
Spacer()
HStack {
Image(systemName: "pencil")
.font(.title)
Text("Введите свою группу")
.font(.system(size: 20, weight: .bold, design: .default))
}
.foregroundColor(Color("blueColor"))
Spacer()
}
}
}
#Preview {
FirstLaunchScheduleView()
}

View File

@ -1,209 +0,0 @@
//
// ScheduleView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 13.11.2024.
//
import SwiftUI
struct MainView: View {
@State private var searchText: String = ""
@State private var currentDate: Date = Date()
@State private var weekSlider: [[Date.WeekDay]] = []
@State private var currentWeekIndex: Int = 1
@State private var createWeek: Bool = false
@State private var isShowingMonthSlider: Bool = false
@State private var isFirstAppearence = true
@ObservedObject var vm: ViewModel
var body: some View {
VStack {
SearchBarView(text: $searchText, vm: vm)
if (vm.isFirstStartOffApp) {
FirstLaunchScheduleView()
}
else {
CurrentDateView()
ScheduleView(vm: vm)
}
}
.alert(isPresented: $vm.isShowingAlertForIncorrectGroup, error: vm.errorInNetwork) { error in
} message: { error in
Text(error.failureReason)
}
.background(Color("background"))
.onAppear(perform: {
currentDate = vm.selectedDay
vm.updateSelectedDayIndex(currentDate)
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())
}
}
})
}
@ViewBuilder
func CurrentDateView() -> some View {
VStack (alignment: .leading, spacing: 6) {
HStack {
VStack (alignment: .leading, spacing: 0) {
Text(currentDate.format("EEEE"))
.font(.system(size: 40, weight: .semibold))
.foregroundStyle(.black)
HStack (spacing: 5) {
Text(currentDate.format("dd"))
.font(.system(size: 20, weight: .bold))
.foregroundStyle(Color("grayForDate"))
Text(currentDate.format("MMMM"))
.font(.system(size: 20, weight: .bold))
.foregroundStyle(Color("grayForDate"))
Spacer()
HStack (spacing: 2) {
Text(isShowingMonthSlider ? "Свернуть" : "Развернуть")
.font(.system(size: 15, weight: .light))
.foregroundStyle(Color.blue)
Image(isShowingMonthSlider ? "arrowup" : "arrowdown")
}
.onTapGesture {
isShowingMonthSlider.toggle()
}
}
}
.padding(.top, 8)
.padding(.leading, 5)
Spacer()
}
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)
}
.onChange(of: currentWeekIndex, initial: false) { oldValue, newValue in
if newValue == 0 || newValue == (weekSlider.count - 1) {
createWeek = true
}
}
.padding(.horizontal)
}
@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, currentDate) ? Color("customGray1") : Color("customGray3"))
.padding(.top, 13)
.foregroundColor(.gray)
Text(day.date.format("dd"))
.font(.system(size: 15, weight: .bold))
.foregroundStyle(isSameDate(day.date, currentDate) ? .white : .black)
.padding(.bottom, 13)
}
.frame(width: 43, height: 55, alignment: .center)
.background( content: {
Group {
if isSameDate(day.date, currentDate) {
Color("blueColor")
}
else {
Color(.white)
}
if isSameDate(day.date, currentDate) {
Color("blueColor")
}
}
}
)
.overlay (
Group {
if day.date.isToday && !isSameDate(day.date, currentDate) {
RoundedRectangle(cornerRadius: 15)
.stroke(Color("blueColor"), lineWidth: 2)
}
}
)
.cornerRadius(15)
.onTapGesture {
currentDate = day.date
vm.updateSelectedDayIndex(currentDate)
}
}
}
.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()
currentDate = vm.selectedDay
}
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()
currentDate = vm.selectedDay
print(currentDate)
}
}
}
}
#Preview {
ContentView()
}

View File

@ -1,79 +0,0 @@
//
// ScheduleView.swift
// Schedule ICTIS
//
// Created by G412 on 05.12.2024.
//
import SwiftUI
struct ScheduleView: View {
@ObservedObject var vm: ViewModel
var body: some View {
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(onlineOrOffline(lesson) ? Color("greenForOffline") : Color("blueForOnline"))
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: 4, y: 4)
}
}
}
}
}
.frame(width: UIScreen.main.bounds.width)
.padding(.bottom, 100)
.padding(.top, 10)
}
}
func convertTimeString(_ input: String) -> [String] {
let parts = input.split(separator: "-")
if let firstPart = parts.first, let lastPart = parts.last {
return [String(firstPart), String(lastPart)]
} else {
return []
}
}
func onlineOrOffline(_ str: String) -> Bool {
if (MockData.onlineClasses.contains(str)) {
return false
}
else {
return true
}
}
}
#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

@ -0,0 +1,44 @@
//
// CommentView.swift
// Schedule ICTIS
//
// Created by G412 on 17.12.2024.
//
import SwiftUI
struct CommentFieldView: View {
@Binding var textForComment: String
@FocusState var isFocused: Bool
var body: some View {
HStack {
TextField("Комментарий", text: $textForComment)
.font(.custom("Montserrat-Medium", fixedSize: 17))
.submitLabel(.done)
.multilineTextAlignment(.leading)
.focused($isFocused)
.padding(.top, 6)
.padding(.bottom, 6)
if isFocused {
Button {
textForComment = ""
self.isFocused = false
} label: {
Image(systemName: "xmark.circle.fill")
.padding(.trailing, 20)
.offset(x: 10)
.foregroundColor(.gray)
}
}
}
.frame(minHeight: 40)
.padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
}
}

View File

@ -0,0 +1,47 @@
//
// ProfessorFieldView.swift
// Schedule ICTIS
//
// Created by G412 on 23.01.2025.
//
import SwiftUI
struct ProfessorFieldView: View {
@Binding var text: String
var labelForField: String
@FocusState var isFocused: Bool
var body: some View {
HStack(spacing: 0) {
Image(systemName: "graduationcap")
.foregroundColor(Color.gray)
.padding(.leading, 12)
.padding(.trailing, 7)
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 {
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

@ -0,0 +1,20 @@
//
// FirstLaunchScheduleView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 06.12.2024.
//
import SwiftUI
struct FirstLaunchScheduleView: View {
var body: some View {
VStack () {
Spacer()
}
}
}
#Preview {
FirstLaunchScheduleView()
}

View File

@ -0,0 +1,97 @@
//
// ScheduleView.swift
// Schedule ICTIS
//
// Created by Mironov Egor on 13.11.2024.
//
import SwiftUI
struct MainView: View {
@State private var searchText: String = ""
@State private var isShowingMonthSlider: Bool = false
@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, 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 {
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)
}
.background(Color("background"))
}
@ViewBuilder
func CurrentDateView() -> some View {
VStack (alignment: .leading, spacing: 6) {
HStack {
VStack (alignment: .leading, spacing: 0) {
Text(vm.selectedDay.format("EEEE"))
.font(.custom("Montserrat-SemiBold", fixedSize: 30))
.foregroundStyle(.black)
HStack (spacing: 5) {
Text(vm.selectedDay.format("dd"))
.font(.custom("Montserrat-Bold", fixedSize: 17))
.foregroundStyle(Color("grayForDate"))
Text(vm.selectedDay.format("MMMM"))
.font(.custom("Montserrat-Bold", fixedSize: 17))
.foregroundStyle(Color("grayForDate"))
Spacer()
Button(action: {
withAnimation(.easeInOut(duration: 0.5)) {
isShowingMonthSlider.toggle()
}
}) {
HStack(spacing: 2) {
Text(isShowingMonthSlider ? "Свернуть" : "Развернуть")
.font(.custom("Montserrat-Regular", fixedSize: 15))
.foregroundStyle(Color.blue)
Image(isShowingMonthSlider ? "arrowup" : "arrowdown")
.resizable()
.scaledToFit()
.frame(width: 15, height: 15)
}
}
}
}
.padding(.top, 8)
.padding(.leading, 5)
Spacer()
}
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)
}
}

View File

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

View File

@ -0,0 +1,262 @@
import SwiftUI
import CoreData
struct ScheduleView: View {
@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
}
}
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
}
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")
}
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}

View File

@ -9,8 +9,12 @@ import SwiftUI
struct SearchBarView: View {
@Binding var text: String
@State private var isEditing = false
@ObservedObject var vm: ViewModel
@FocusState var isFocused: Bool
@State private var isShowingSheet: Bool = false
@ObservedObject var vm: ScheduleViewModel
@Binding var isShowingMonthSlider: Bool
var provider = ClassProvider.shared
var body: some View {
HStack (spacing: 11) {
@ -21,22 +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) {
vm.fetchWeekSchedule(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)
@ -45,16 +52,19 @@ struct SearchBarView: View {
.background(
)
}
.background(Color.white)
}
}
.simultaneousGesture(TapGesture().onEnded {
self.isShowingMonthSlider = false
})
.frame(height: 40)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
if (!vm.isFirstStartOffApp && !vm.isShowingAlertForIncorrectGroup) {
if !isFocused {
Button {
isShowingSheet = true
} label: {
ZStack {
Rectangle()
@ -74,9 +84,9 @@ struct SearchBarView: View {
.padding(.top, 5)
.frame(height: 40)
.accentColor(.blue)
.sheet(isPresented: $isShowingSheet) {
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

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

@ -2,20 +2,23 @@
// MockData.swift
// Schedule ICTIS
//
// Created by G412 on 06.12.2024.
// Created by Mironov Egor on 06.12.2024.
//
import Foundation
struct MockData {
static let onlineClasses: [String] = [
"пр.Академический курс иностранного языка Янкаускас Е. С. LMS",
"пр.Академический курс иностранного языка Янкаускас Е. С. LMS-3",
"пр.Введение в инженерную деятельность 1 п/г Михайлова В. Д. LMS 2 п/г Романенко К. С. 3 п/г Козловский А. В. 4 п/г Компаниец В. С. 5 п/г Олейников К. А. 6 п/г Прудников В. А. 7 п/г Петров Д. А. 8 п/г Григорян К. С.",
"пр.Иностранный язык Иностранный язык LMS",
"лек.Операционные системы 1 п/г Шкурко А. Н. Г-309 АКТРУ 2 п/г Дроздов С. Н. Г-333 3 п/г Нужнов Е. В. Г-301 Operating systems(Операционные системы) 4 п/г Самойлов А. Н. LMS",
"пр.Введение в инженерную деятельность 1 п/г Плёнкин А. П. LMS-1 2 п/г Кучеров С. А. 3 п/г Шкурко А. Н. 4 п/г Механцев Б. Е.",
"лек.Операционные системы 1 п/г Шкурко А. Н. Г-309 АКТРУ 2 п/г Дроздов С. Н. Г-333 3 п/г Нужнов Е. В. Г-301 Operating systems(Операционные системы) 4 п/г Самойлов А. Н. LMS"
]
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

@ -1,6 +1,7 @@
{
"images" : [
{
"filename" : "ICTIS_logo.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

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

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,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x99",
"green" : "0x99",
"red" : "0x99"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x99",
"green" : "0x99",
"red" : "0x99"
}
},
"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,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xDE",
"green" : "0xE4",
"red" : "0x22"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xDE",
"green" : "0xE4",
"red" : "0x22"
}
},
"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()
}

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

@ -26,12 +26,6 @@ extension Date {
return Calendar.current.isDateInToday(self)
}
private func isSameDate(_ date1: Date?, _ date2: Date?) -> Bool {
guard let date1 = date1, let date2 = date2 else { return false }
let calendar = Calendar.current
return calendar.isDate(date1, inSameDayAs: date2)
}
func fetchWeek(_ date: Date = .init()) -> [WeekDay] {
let calendar = Calendar.current
let startOfDate = calendar.startOfDay(for: date)
@ -56,6 +50,51 @@ extension Date {
return week
}
func fetchMonth(_ date: Date = .init()) -> [MonthWeek] {
let calendar = Calendar.current
let startOfDate = calendar.startOfDay(for: date)
let weekForDate = calendar.dateInterval(of: .weekOfMonth, for: startOfDate)
guard let startOfWeek = weekForDate?.start else {
return []
}
var month: [MonthWeek] = []
for weekIndex in 0..<5 {
var week: [WeekDay] = []
for dayIndex in 0..<7 {
if let weekDay = calendar.date(byAdding: .day, value: (weekIndex * 7 + dayIndex), to: startOfWeek) {
week.append(WeekDay(date: weekDay))
}
}
month.append(MonthWeek(week: week))
}
return month
}
func createNextMonth() -> [MonthWeek] {
let calendar = Calendar.current
let startOfLastDate = calendar.startOfDay(for: self)
guard let nextDate = calendar.date(byAdding: .day, value: 1, to: startOfLastDate) else {
return []
}
return fetchMonth(nextDate)
}
func createPreviousMonth() -> [MonthWeek] {
let calendar = Calendar.current
let startOfFirstDate = calendar.startOfDay(for: self)
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)
}
func createNextWeek() -> [WeekDay] {
let calendar = Calendar.current
let startOfLastDate = calendar.startOfDay(for: self)
@ -78,4 +117,9 @@ extension Date {
var id: UUID = .init()
var date: Date
}
struct MonthWeek: Identifiable {
var id: UUID = .init()
var week: [WeekDay]
}
}

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