Compare commits
7 Commits
13de6fa302
...
main
Author | SHA1 | Date | |
---|---|---|---|
edfe97c6dc | |||
14c229175c | |||
99f2bd8a74 | |||
8bc7425e2a | |||
5946cd3ec0 | |||
9bfd85ec3d | |||
15fbe5895c |
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
||||
<entity name="ClassModel" representedClassName=".ClassModel" syncable="YES">
|
||||
<entity name="CoreDataClassModel" representedClassName=".CoreDataClassModel" syncable="YES">
|
||||
<attribute name="auditory" optional="YES" attributeType="String"/>
|
||||
<attribute name="comment" optional="YES" attributeType="String"/>
|
||||
<attribute name="day" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
@ -12,4 +12,11 @@
|
||||
<attribute name="starttime" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="subject" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="JsonClassModel" representedClassName="JsonClassModel" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="day" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="group" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="time" optional="YES" attributeType="String"/>
|
||||
<attribute name="week" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
</model>
|
@ -8,7 +8,7 @@
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "CF2C0E34-74B0-458B-AE66-E61DEB75A958"
|
||||
shouldBeEnabled = "No"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Schedule ICTIS/Main/Views/ProfessorAuditoryClassFieldView.swift"
|
||||
|
32
Schedule ICTIS/ConnectingToNetworkView.swift
Normal file
32
Schedule ICTIS/ConnectingToNetworkView.swift
Normal 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()
|
||||
}
|
@ -8,47 +8,70 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var selectedTab: Int = 1
|
||||
@StateObject var vm = ScheduleViewModel()
|
||||
|
||||
@State private var selectedTab: TabBarModel = .schedule
|
||||
@State private var isTabBarHidden = false
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
@ObservedObject var networkMonitor: NetworkMonitor
|
||||
var body: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
Text("Tasks")
|
||||
.tabItem {
|
||||
Image(systemName: "books.vertical")
|
||||
Text("Задания")
|
||||
}
|
||||
.tag(0)
|
||||
|
||||
MainView(vm: vm)
|
||||
.tabItem {
|
||||
Image(systemName: "house")
|
||||
Text("Расписание")
|
||||
}
|
||||
.tag(1)
|
||||
|
||||
SettingsView(vm: vm)
|
||||
.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 {
|
||||
let group = UserDefaults.standard.string(forKey: "group")
|
||||
if let nameGroup = group {
|
||||
vm.group = nameGroup
|
||||
vm.fetchWeekSchedule(group: nameGroup)
|
||||
}
|
||||
if let vpkStr = UserDefaults.standard.string(forKey: "vpk") {
|
||||
vm.fetchWeekVPK(vpk: vpkStr)
|
||||
}
|
||||
print(vm.vpks)
|
||||
vm.fetchWeekSchedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
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)
|
||||
}
|
||||
|
31
Schedule ICTIS/ErrorsView/NetworkErrorView.swift
Normal file
31
Schedule ICTIS/ErrorsView/NetworkErrorView.swift
Normal 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: "Восстановите подключение к интернету чтобы мы смогли загрузить расписание")
|
||||
}
|
@ -1,10 +1,3 @@
|
||||
//
|
||||
// LoadingScheduleView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by G412 on 19.02.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoadingScheduleView: View {
|
||||
@ -13,27 +6,43 @@ struct LoadingScheduleView: View {
|
||||
ZStack {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 20) {
|
||||
ForEach(0..<6, id: \.self) { _ in
|
||||
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
|
||||
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(height: 70)
|
||||
.padding(.horizontal, 20)
|
||||
.animation(.linear(duration: 0.8).repeatForever(autoreverses: true), value: isAnimated)
|
||||
.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, 30)
|
||||
.padding(.top, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,24 +2,28 @@
|
||||
// LoadingView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 11.12.2024.
|
||||
// Created by Mironov Egor on 04.04.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoadingView: View {
|
||||
@Binding var isLoading: Bool
|
||||
@State private var isAnimating = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color("background")
|
||||
.ignoresSafeArea()
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .secondary))
|
||||
.scaleEffect(1)
|
||||
}
|
||||
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(isLoading: .constant(true))
|
||||
LoadingView()
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CreatedClassView: View {
|
||||
@ObservedObject var _class: ClassModel
|
||||
@ObservedObject var _class: CoreDataClassModel
|
||||
var provider = ClassProvider.shared
|
||||
var body: some View {
|
||||
let existingCopy = try? provider.viewContext.existingObject(with: _class.objectID)
|
||||
|
50
Schedule ICTIS/Main/Views/FilterGroupsView.swift
Normal file
50
Schedule ICTIS/Main/Views/FilterGroupsView.swift
Normal 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)
|
||||
}
|
@ -11,9 +11,9 @@ 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)
|
||||
@ -23,11 +23,12 @@ struct MainView: View {
|
||||
}
|
||||
}
|
||||
CurrentDateView()
|
||||
FilterGroupsView(vm: vm)
|
||||
if vm.isLoading {
|
||||
LoadingScheduleView()
|
||||
}
|
||||
else {
|
||||
ScheduleView(vm: vm, isScrolling: $isScrolling)
|
||||
ScheduleView(vm: vm, networkMonitor: networkMonitor, isScrolling: $isScrolling)
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $vm.isShowingAlertForIncorrectGroup, error: vm.errorInNetwork) { error in
|
||||
@ -82,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()
|
||||
}
|
||||
|
||||
|
@ -1,189 +1,255 @@
|
||||
//
|
||||
// ScheduleView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 05.12.2024.
|
||||
// ктбо2-6
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct ScheduleView: View {
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
@FetchRequest(fetchRequest: ClassModel.all()) private var classes // Делаем запрос в CoreData и получаем список сохраненных пар
|
||||
@State private var selectedClass: ClassModel? = nil
|
||||
@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
|
||||
@State private var isShowingMyPairs = false
|
||||
@Binding var isScrolling: Bool
|
||||
var provider = ClassProvider.shared
|
||||
var hasLessons: Bool {
|
||||
return vm.classes.indices.contains(vm.selectedIndex) &&
|
||||
vm.classes[vm.selectedIndex].dropFirst().contains { !$0.isEmpty }
|
||||
}
|
||||
var hasVPK: Bool {
|
||||
return vm.vpks.indices.contains(vm.selectedIndex) && vm.vpks[vm.selectedIndex].dropFirst().contains { !$0.isEmpty }
|
||||
}
|
||||
var body: some View {
|
||||
if vm.isLoading {
|
||||
LoadingScheduleView()
|
||||
|
||||
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: 30) {
|
||||
VStack (alignment: .leading, spacing: 20 ) {
|
||||
if hasLessons {
|
||||
Text("Учебное расписание")
|
||||
.font(.custom("Montserrat-Bold", fixedSize: 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: 15) {
|
||||
VStack {
|
||||
Text(convertTimeString(vm.classes[1][lessonIndex])[0])
|
||||
.font(.custom("Montserrat-Regular", fixedSize: 15))
|
||||
.padding(.bottom, 1)
|
||||
Text(convertTimeString(vm.classes[1][lessonIndex])[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(lesson))
|
||||
Text(lesson)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if UserDefaults.standard.string(forKey: "vpk") != nil {
|
||||
VStack (alignment: .leading, spacing: 20 ) {
|
||||
if hasVPK {
|
||||
Text("ВПК")
|
||||
.font(.custom("Montserrat-Bold", fixedSize: 20))
|
||||
}
|
||||
ForEach(vm.vpks.indices, id: \.self) { index in
|
||||
if index != 0 && index != 1 && index == vm.selectedIndex {
|
||||
let dayVPK = vm.vpks[index] // Это массив строк для дня
|
||||
ForEach(dayVPK.indices.dropFirst(), id: \.self) { lessonIndex in
|
||||
let lesson = dayVPK[lessonIndex] // Это строка с расписанием одной пары
|
||||
if !lesson.isEmpty {
|
||||
HStack(spacing: 15) {
|
||||
VStack {
|
||||
Text(convertTimeString(vm.vpks[1][lessonIndex])[0])
|
||||
.font(.custom("Montserrat-Regular", fixedSize: 15))
|
||||
.padding(.bottom, 1)
|
||||
Text(convertTimeString(vm.vpks[1][lessonIndex])[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(lesson))
|
||||
Text(lesson)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
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 будет открываться, когда selectedClass будет становиться не nil
|
||||
.sheet(item: $selectedClass,
|
||||
onDismiss: {
|
||||
selectedClass = nil
|
||||
},
|
||||
content: { _class in
|
||||
CreateEditClassView(vm: .init(provider: provider, _class: _class), day: vm.selectedDay)
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,7 +260,3 @@ struct ViewOffsetKey: PreferenceKey {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
}
|
||||
|
@ -29,7 +29,13 @@ struct SearchBarView: View {
|
||||
.onSubmit {
|
||||
self.isFocused = false
|
||||
if (!text.isEmpty) {
|
||||
vm.fetchWeekSchedule(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 = ""
|
||||
}
|
||||
@ -84,6 +90,3 @@ struct SearchBarView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
}
|
||||
|
61
Schedule ICTIS/Main/Views/SubjectView.swift
Normal file
61
Schedule ICTIS/Main/Views/SubjectView.swift
Normal 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])
|
||||
}
|
||||
}
|
||||
}
|
@ -15,31 +15,31 @@ struct MonthTabView: View {
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack (spacing: 34) {
|
||||
HStack (spacing: 33) {
|
||||
ForEach(MockData.daysOfWeek.indices, id: \.self) { index in
|
||||
Text(MockData.daysOfWeek[index])
|
||||
.font(.custom("Montserrat-SemiBold", fixedSize: 15))
|
||||
.foregroundColor(MockData.daysOfWeek[index] == "Вс" ? Color(.red) : Color("customGray2"))
|
||||
.padding(.top, 13)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.padding(.top, 14)
|
||||
TabView(selection: $currentMonthIndex) {
|
||||
ForEach(monthSlider.indices, id: \.self) { index in
|
||||
let month = monthSlider[index]
|
||||
MonthView(month)
|
||||
.tag(index)
|
||||
.transition(.slide)
|
||||
}
|
||||
}
|
||||
.padding(.top, -25)
|
||||
.padding(.bottom, -10)
|
||||
.padding(.horizontal, -15)
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
//.animation(.easeIn(duration: 0.3), value: currentMonthIndex)
|
||||
}
|
||||
.onAppear(perform: {
|
||||
.frame(height: 220)
|
||||
.padding(.top, 26)
|
||||
.padding(.bottom, 20)
|
||||
.onAppear {
|
||||
updateMonthScreenViewForNewGroup()
|
||||
})
|
||||
}
|
||||
.onChange(of: currentMonthIndex, initial: false) { oldValue, newValue in
|
||||
if newValue == 0 || newValue == (monthSlider.count - 1) {
|
||||
createMonth = true
|
||||
@ -82,6 +82,3 @@ struct MonthTabView: View {
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
}
|
||||
|
@ -44,7 +44,3 @@ struct WeekTabView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
final class ClassModel: NSManagedObject, Identifiable {
|
||||
final class CoreDataClassModel: NSManagedObject, Identifiable {
|
||||
@NSManaged var auditory: String
|
||||
@NSManaged var professor: String
|
||||
@NSManaged var subject: String
|
||||
@ -47,29 +47,29 @@ final class ClassModel: NSManagedObject, Identifiable {
|
||||
}
|
||||
|
||||
// Расширение для загрузки данных из памяти
|
||||
extension ClassModel {
|
||||
extension CoreDataClassModel {
|
||||
// Получаем все данные из памяти
|
||||
private static var classesFetchRequest: NSFetchRequest<ClassModel> {
|
||||
NSFetchRequest(entityName: "ClassModel")
|
||||
private static var classesFetchRequest: NSFetchRequest<CoreDataClassModel> {
|
||||
NSFetchRequest(entityName: "CoreDataClassModel")
|
||||
}
|
||||
|
||||
// Получаем все данные и сортируем их по дню
|
||||
// Этот метод будет использоваться на View(ScheduleView), где отображаются пары
|
||||
static func all() -> NSFetchRequest<ClassModel> {
|
||||
let request: NSFetchRequest<ClassModel> = classesFetchRequest
|
||||
static func all() -> NSFetchRequest<CoreDataClassModel> {
|
||||
let request: NSFetchRequest<CoreDataClassModel> = classesFetchRequest
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \ClassModel.day, ascending: true)
|
||||
NSSortDescriptor(keyPath: \CoreDataClassModel.day, ascending: true)
|
||||
]
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
extension ClassModel {
|
||||
extension CoreDataClassModel {
|
||||
@discardableResult
|
||||
static func makePreview(count: Int, in context: NSManagedObjectContext) -> [ClassModel] {
|
||||
var classes = [ClassModel]()
|
||||
static func makePreview(count: Int, in context: NSManagedObjectContext) -> [CoreDataClassModel] {
|
||||
var classes = [CoreDataClassModel]()
|
||||
for i in 0..<count {
|
||||
let _class = ClassModel(context: context)
|
||||
let _class = CoreDataClassModel(context: context)
|
||||
_class.subject = "Предмет \(i)"
|
||||
_class.auditory = "Аудитория \(i)"
|
||||
_class.professor = "Преподаватель \(i)"
|
||||
@ -81,11 +81,11 @@ extension ClassModel {
|
||||
return classes
|
||||
}
|
||||
|
||||
static func preview(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> ClassModel {
|
||||
static func preview(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel {
|
||||
return makePreview(count: 1, in: context)[0]
|
||||
}
|
||||
|
||||
static func empty(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> ClassModel {
|
||||
return ClassModel(context: context)
|
||||
static func empty(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel {
|
||||
return CoreDataClassModel(context: context)
|
||||
}
|
||||
}
|
@ -9,11 +9,11 @@ import Foundation
|
||||
|
||||
// MARK: - Welcome
|
||||
struct Welcome: Decodable {
|
||||
let choices: [Choice]
|
||||
let choices: [Subject]
|
||||
}
|
||||
|
||||
// MARK: - Choice
|
||||
struct Choice: Decodable, Identifiable {
|
||||
struct Subject: Decodable, Identifiable {
|
||||
let name: String
|
||||
let id: String
|
||||
let group: String
|
||||
|
46
Schedule ICTIS/Model/JsonClassModel.swift
Normal file
46
Schedule ICTIS/Model/JsonClassModel.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
import SwiftUI
|
||||
|
||||
enum TabBarModel: String, CaseIterable {
|
||||
case schedule = "house"
|
||||
case tasks = "books.vertical"
|
||||
case schedule = "house"
|
||||
case settings = "gear"
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -49,11 +49,11 @@ final class ClassProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func exists(_ lesson: ClassModel, in context: NSManagedObjectContext) -> ClassModel? {
|
||||
try? context.existingObject(with: lesson.objectID) as? ClassModel
|
||||
func exists(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) -> CoreDataClassModel? {
|
||||
try? context.existingObject(with: lesson.objectID) as? CoreDataClassModel
|
||||
}
|
||||
|
||||
func delete(_ lesson: ClassModel, in context: NSManagedObjectContext) throws {
|
||||
func delete(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) throws {
|
||||
if let existingClass = exists(lesson, in: context) {
|
||||
context.delete(existingClass)
|
||||
Task(priority: .background) {
|
||||
@ -76,3 +76,20 @@ extension EnvironmentValues {
|
||||
return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||
}
|
||||
}
|
||||
|
||||
extension ClassProvider {
|
||||
func exists(_ jsonClass: JsonClassModel, in context: NSManagedObjectContext) -> JsonClassModel? {
|
||||
try? context.existingObject(with: jsonClass.objectID) as? JsonClassModel
|
||||
}
|
||||
|
||||
func delete(_ jsonClass: JsonClassModel, in context: NSManagedObjectContext) throws {
|
||||
if let existingJsonClass = exists(jsonClass, in: context) {
|
||||
context.delete(existingJsonClass)
|
||||
Task(priority: .background) {
|
||||
try await context.perform {
|
||||
try context.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,16 @@ import SwiftUI
|
||||
|
||||
@main
|
||||
struct Schedule_ICTISApp: App {
|
||||
@StateObject private var networkMonitor = NetworkMonitor()
|
||||
@StateObject var vm = ScheduleViewModel()
|
||||
var provider = ClassProvider.shared
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
ContentView(vm: vm, networkMonitor: networkMonitor)
|
||||
.environment(\.managedObjectContext, ClassProvider.shared.viewContext)
|
||||
.onAppear {
|
||||
vm.fillDictForVm()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
101
Schedule ICTIS/Settings/FavGroupsView.swift
Normal file
101
Schedule ICTIS/Settings/FavGroupsView.swift
Normal 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)
|
||||
}
|
101
Schedule ICTIS/Settings/FavVPKView.swift
Normal file
101
Schedule ICTIS/Settings/FavVPKView.swift
Normal 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)
|
||||
}
|
55
Schedule ICTIS/Settings/ListOfGroupsView.swift
Normal file
55
Schedule ICTIS/Settings/ListOfGroupsView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,20 +8,16 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ScheduleGroupSettings: View {
|
||||
@AppStorage("group") private var favGroup = ""
|
||||
@AppStorage("vpk") private var favVPK = ""
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
@ObservedObject var networkMonitor: NetworkMonitor
|
||||
var body: some View {
|
||||
VStack {
|
||||
NavigationLink(destination: SelectingGroupView(vm: vm)) {
|
||||
NavigationLink(destination: FavGroupsView(vm: vm, networkMonitor: networkMonitor)) {
|
||||
HStack {
|
||||
Text("Избранное расписание")
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.foregroundColor(.black)
|
||||
Spacer()
|
||||
Text(favGroup)
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.foregroundColor(Color("customGray3"))
|
||||
Image("arrowRight")
|
||||
}
|
||||
.padding(.horizontal)
|
||||
@ -32,15 +28,12 @@ struct ScheduleGroupSettings: View {
|
||||
.foregroundColor(Color("customGray1"))
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal)
|
||||
NavigationLink(destination: SelectingVPKView(vm: vm)) {
|
||||
NavigationLink(destination: FavVPKView(vm: vm, networkMonitor: networkMonitor)) {
|
||||
HStack {
|
||||
Text("ВПК")
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.foregroundColor(.black)
|
||||
Spacer()
|
||||
Text(favVPK)
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.foregroundColor(Color("customGray3"))
|
||||
Image("arrowRight")
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
@ -12,144 +12,143 @@ struct SelectingGroupView: View {
|
||||
@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?
|
||||
@AppStorage("group") private var favGroup = ""
|
||||
@StateObject private var serchGroupsVM = SearchGroupsViewModel()
|
||||
var firstFavGroup: String
|
||||
var secondFavGroup: String
|
||||
var thirdFavGroup: String
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
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 {
|
||||
vm.fetchGroups(group: text)
|
||||
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.fetchGroups(group: "кт")
|
||||
}
|
||||
}
|
||||
searchTask = task
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: task)
|
||||
}
|
||||
.onSubmit {
|
||||
self.isFocused = false
|
||||
if (!text.isEmpty) {
|
||||
vm.fetchWeekSchedule(group: text)
|
||||
self.isLoading = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.isLoading = false
|
||||
if vm.errorInNetwork == .noError {
|
||||
vm.errorInNetwork = nil
|
||||
print("Зашел")
|
||||
UserDefaults.standard.set(text, forKey: "group")
|
||||
vm.group = text
|
||||
self.text = ""
|
||||
dismiss()
|
||||
}
|
||||
else {
|
||||
vm.isShowingAlertForIncorrectGroup = true
|
||||
vm.errorInNetwork = .invalidResponse
|
||||
}
|
||||
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(isLoading: $isLoading)
|
||||
}
|
||||
.submitLabel(.done)
|
||||
if isFocused {
|
||||
ScrollView(.vertical, showsIndicators: true) {
|
||||
ForEach(vm.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 {
|
||||
UserDefaults.standard.set(item.name, forKey: "group")
|
||||
vm.group = item.name
|
||||
vm.fetchWeekSchedule(group: item.name)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
self.text = ""
|
||||
self.isFocused = false
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.padding(.trailing, 20)
|
||||
.offset(x: 10)
|
||||
.foregroundColor(.gray)
|
||||
.background(
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if favGroup != "" {
|
||||
Button {
|
||||
UserDefaults.standard.removeObject(forKey: "group")
|
||||
vm.classes.removeAll()
|
||||
vm.group = ""
|
||||
vm.numOfGroup = ""
|
||||
dismiss()
|
||||
} 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)
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.background(Color("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 {
|
||||
vm.fetchGroups(group: "кт")
|
||||
serchGroupsVM.fetchGroups(group: "кт")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
@Previewable @StateObject var vm = ScheduleViewModel()
|
||||
SelectingGroupView(vm: vm)
|
||||
@Previewable @StateObject var vm2 = NetworkMonitor()
|
||||
SelectingGroupView(vm: vm, networkMonitor: vm2, firstFavGroup: "", secondFavGroup: "", thirdFavGroup: "")
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// SelectedVPKView.swift
|
||||
// SelectedGroupView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 30.01.2025.
|
||||
@ -12,145 +12,107 @@ struct SelectingVPKView: View {
|
||||
@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?
|
||||
@AppStorage("vpk") private var favVPK = ""
|
||||
@StateObject private var serchGroupsVM = SearchGroupsViewModel()
|
||||
var firstFavVPK: String
|
||||
var secondFavVPK: String
|
||||
var thirdFavVPK: String
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
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 {
|
||||
vm.fetchGroups(group: text)
|
||||
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.fetchGroups(group: "впк")
|
||||
}
|
||||
}
|
||||
searchTask = task
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: task)
|
||||
}
|
||||
.onSubmit {
|
||||
self.isFocused = false
|
||||
if (!text.isEmpty) {
|
||||
vm.fetchWeekVPK(vpk: UserDefaults.standard.string(forKey: "vpk"))
|
||||
self.isLoading = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.isLoading = false
|
||||
if vm.errorInNetwork == .noError {
|
||||
vm.errorInNetwork = nil
|
||||
print("Зашел")
|
||||
UserDefaults.standard.set(text, forKey: "vpk")
|
||||
vm.group = text
|
||||
self.text = ""
|
||||
dismiss()
|
||||
}
|
||||
else {
|
||||
vm.isShowingAlertForIncorrectGroup = true
|
||||
vm.errorInNetwork = .invalidResponse
|
||||
}
|
||||
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(isLoading: $isLoading)
|
||||
}
|
||||
.submitLabel(.done)
|
||||
if isFocused {
|
||||
ScrollView(.vertical, showsIndicators: true) {
|
||||
ForEach(vm.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 {
|
||||
UserDefaults.standard.set(item.name, forKey: "vpk")
|
||||
vm.vpk = item.name
|
||||
vm.fetchWeekVPK(vpk: item.name)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
self.text = ""
|
||||
self.isFocused = false
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.padding(.trailing, 20)
|
||||
.offset(x: 10)
|
||||
.foregroundColor(.gray)
|
||||
.background(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isFocused {
|
||||
if favVPK != "" {
|
||||
Button {
|
||||
UserDefaults.standard.removeObject(forKey: "vpk")
|
||||
vm.vpks.removeAll()
|
||||
vm.vpk = ""
|
||||
vm.vpkHTML = ""
|
||||
dismiss()
|
||||
} 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)
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.background(Color("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 {
|
||||
vm.fetchGroups(group: "впк")
|
||||
serchGroupsVM.fetchGroups(group: "ВПК")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
@Previewable @StateObject var vm = ScheduleViewModel()
|
||||
SelectingVPKView(vm: vm)
|
||||
@Previewable @StateObject var vm2 = NetworkMonitor()
|
||||
SelectingVPKView(vm: vm, networkMonitor: vm2, firstFavVPK: "", secondFavVPK: "", thirdFavVPK: "")
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ 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 {
|
||||
@ -28,7 +29,7 @@ struct SettingsView: View {
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 18))
|
||||
.foregroundColor(Color("customGray3"))
|
||||
.padding(.horizontal)
|
||||
ScheduleGroupSettings(vm: vm)
|
||||
ScheduleGroupSettings(vm: vm, networkMonitor: networkMonitor)
|
||||
}
|
||||
.padding(.top, 20)
|
||||
}
|
||||
@ -42,5 +43,6 @@ struct SettingsView: View {
|
||||
|
||||
#Preview {
|
||||
@Previewable @StateObject var vm = ScheduleViewModel()
|
||||
SettingsView(vm: vm)
|
||||
@Previewable @StateObject var vm2 = NetworkMonitor()
|
||||
SettingsView(vm: vm, networkMonitor: vm2)
|
||||
}
|
||||
|
28
Schedule ICTIS/Settings/TestingView.swift
Normal file
28
Schedule ICTIS/Settings/TestingView.swift
Normal 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()
|
||||
}
|
@ -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,14 +20,14 @@ 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)),
|
||||
// in: .capsule
|
||||
// )
|
||||
//.background(
|
||||
// background
|
||||
// .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
|
||||
//)
|
||||
}
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom) // Фиксаци таб-бара, при появлении клавиатуры
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
@ -87,9 +87,11 @@ extension Date {
|
||||
func createPreviousMonth() -> [MonthWeek] {
|
||||
let calendar = Calendar.current
|
||||
let startOfFirstDate = calendar.startOfDay(for: self)
|
||||
guard let previousDate = calendar.date(byAdding: .month, value: -1, to: startOfFirstDate) else {
|
||||
guard let previousDate = calendar.date(byAdding: .weekOfMonth, value: -5, to: startOfFirstDate) else {
|
||||
return []
|
||||
}
|
||||
print("Start of first date \(startOfFirstDate)")
|
||||
print("Previous date \(previousDate)")
|
||||
return fetchMonth(previousDate)
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,36 @@
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
|
||||
func transformStringToFormat(_ input: String) -> String {
|
||||
var result = input
|
||||
|
||||
// Условие 1: начинается с "кт"
|
||||
if result.lowercased().hasPrefix("кт") {
|
||||
result = result.lowercased()
|
||||
let firstTwo = String(result.prefix(2)).uppercased()
|
||||
let rest = String(result.dropFirst(2))
|
||||
result = firstTwo + rest
|
||||
return result
|
||||
}
|
||||
|
||||
// Условие 2: содержит "впк"
|
||||
if result.lowercased().contains("впк") {
|
||||
result = result.lowercased()
|
||||
result = result.replacingOccurrences(of: "впк", with: "ВПК")
|
||||
return result
|
||||
}
|
||||
|
||||
// Условие 3: содержит "мвпк"
|
||||
if result.lowercased().contains("мвпк") {
|
||||
result = result.lowercased()
|
||||
result = result.replacingOccurrences(of: "впк", with: "ВПК")
|
||||
return result
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func isSameDate(_ date1: Date, _ date2: Date) -> Bool {
|
||||
return Calendar.current.isDate(date1, inSameDayAs: date2)
|
||||
}
|
||||
@ -183,15 +213,16 @@ extension WeekTabView {
|
||||
extension WeekViewForWeek {
|
||||
func paginateWeek() {
|
||||
let calendar = Calendar.current
|
||||
let groupsKeys = Array (vm.nameToHtml.keys)
|
||||
if weekSlider.indices.contains(currentWeekIndex) {
|
||||
if let firstDate = weekSlider[currentWeekIndex].first?.date,
|
||||
currentWeekIndex == 0 {
|
||||
vm.week -= 1
|
||||
if vm.group != "" {
|
||||
if !groupsKeys.isEmpty {
|
||||
vm.fetchWeekSchedule(isOtherWeek: true)
|
||||
}
|
||||
if UserDefaults.standard.string(forKey: "vpk") != nil {
|
||||
vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk"))
|
||||
//vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk"))
|
||||
}
|
||||
weekSlider.insert(firstDate.createPrevioustWeek(), at: 0)
|
||||
weekSlider.removeLast()
|
||||
@ -203,12 +234,9 @@ extension WeekViewForWeek {
|
||||
if let lastDate = weekSlider[currentWeekIndex].last?.date,
|
||||
currentWeekIndex == (weekSlider.count - 1) {
|
||||
vm.week += 1
|
||||
if vm.group != "" {
|
||||
if !groupsKeys.isEmpty {
|
||||
vm.fetchWeekSchedule(isOtherWeek: true)
|
||||
}
|
||||
if UserDefaults.standard.string(forKey: "vpk") != nil {
|
||||
vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk"))
|
||||
}
|
||||
weekSlider.append(lastDate.createNextWeek())
|
||||
weekSlider.removeFirst()
|
||||
currentWeekIndex = weekSlider.count - 2
|
||||
@ -246,17 +274,18 @@ extension WeekViewForMonth {
|
||||
print("На одной неделе")
|
||||
}
|
||||
else {
|
||||
let groupsKeys = Array(vm.nameToHtml.keys)
|
||||
var difBetweenWeeks = weeksBetween(startDate: vm.selectedDay, endDate: day.date)
|
||||
if day.date < vm.selectedDay {
|
||||
difBetweenWeeks = difBetweenWeeks * -1
|
||||
}
|
||||
print(difBetweenWeeks)
|
||||
vm.week += difBetweenWeeks
|
||||
if vm.group != "" {
|
||||
if !groupsKeys.isEmpty {
|
||||
vm.fetchWeekSchedule(isOtherWeek: true)
|
||||
}
|
||||
if UserDefaults.standard.string(forKey: "vpk") != nil {
|
||||
vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk"))
|
||||
//vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk"))
|
||||
}
|
||||
}
|
||||
vm.selectedDay = day.date
|
||||
@ -271,19 +300,24 @@ extension MonthTabView {
|
||||
let currentMonth = Date().fetchMonth(vm.selectedDay)
|
||||
|
||||
if let firstDate = currentMonth.first?.week[0].date {
|
||||
monthSlider.append(firstDate.createPreviousMonth())
|
||||
let temp = firstDate.createPreviousMonth()
|
||||
print("First date - \(firstDate)")
|
||||
print(temp)
|
||||
monthSlider.append(temp)
|
||||
}
|
||||
|
||||
monthSlider.append(currentMonth)
|
||||
|
||||
if let lastDate = currentMonth.last?.week[6].date {
|
||||
monthSlider.append(lastDate.createNextMonth())
|
||||
let temp = lastDate.createNextMonth()
|
||||
monthSlider.append(temp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func paginateMonth(_ indexOfWeek: Int = 0) {
|
||||
let calendar = Calendar.current
|
||||
let groupsKeys = Array (vm.nameToHtml.keys)
|
||||
if monthSlider.indices.contains(currentMonthIndex) {
|
||||
if let firstDate = monthSlider[currentMonthIndex].first?.week[0].date,
|
||||
currentMonthIndex == 0 {
|
||||
@ -293,12 +327,9 @@ extension MonthTabView {
|
||||
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -5, to: vm.selectedDay) ?? Date.init()
|
||||
vm.updateSelectedDayIndex()
|
||||
vm.week -= 5
|
||||
if vm.group != "" {
|
||||
if !groupsKeys.isEmpty {
|
||||
vm.fetchWeekSchedule(isOtherWeek: true)
|
||||
}
|
||||
if let vpkStr = UserDefaults.standard.string(forKey: "vpk") {
|
||||
vm.fetchWeekVPK(vpk: vpkStr)
|
||||
}
|
||||
}
|
||||
|
||||
if let lastDate = monthSlider[currentMonthIndex].last?.week[6].date,
|
||||
@ -309,12 +340,9 @@ extension MonthTabView {
|
||||
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 5, to: vm.selectedDay) ?? Date.init()
|
||||
vm.updateSelectedDayIndex()
|
||||
vm.week += 5
|
||||
if vm.group != "" {
|
||||
if !groupsKeys.isEmpty {
|
||||
vm.fetchWeekSchedule(isOtherWeek: true)
|
||||
}
|
||||
if let vpkStr = UserDefaults.standard.string(forKey: "vpk") {
|
||||
vm.fetchWeekVPK(vpk: vpkStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,8 @@ enum NetworkError: String, Error, LocalizedError {
|
||||
case invalidUrl
|
||||
case invalidResponse
|
||||
case invalidData
|
||||
case noNetwork
|
||||
case noError
|
||||
case timeout
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
@ -22,8 +22,8 @@ enum NetworkError: String, Error, LocalizedError {
|
||||
"InvalidResponse"
|
||||
case .invalidData:
|
||||
"Проверьте номер группы"
|
||||
case .noNetwork:
|
||||
"No network connection"
|
||||
case .timeout:
|
||||
"Ошибка сети"
|
||||
case .noError:
|
||||
"Нет ошибки"
|
||||
}
|
||||
@ -37,8 +37,8 @@ enum NetworkError: String, Error, LocalizedError {
|
||||
"Для этой недели расписания еще нет"
|
||||
case .invalidData:
|
||||
"Похоже такой группы не существует"
|
||||
case .noNetwork:
|
||||
"Проверьте подключение к интернету и попробуйте заново"
|
||||
case .timeout:
|
||||
"Проверьте соединение с интернетом"
|
||||
case .noError:
|
||||
"Ошибки нет"
|
||||
}
|
||||
|
@ -14,9 +14,14 @@ final class NetworkManager {
|
||||
private let decoder = JSONDecoder()
|
||||
private let urlForGroup = "https://webictis.sfedu.ru/schedule-api/?query="
|
||||
private let urlForWeek = "https://webictis.sfedu.ru/schedule-api/?group="
|
||||
private let customSession: URLSession // Кастомная сессия для ограничения времени ответа от сервера
|
||||
|
||||
//MARK: Initializer
|
||||
private init() {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.timeoutIntervalForRequest = 3 // Таймаут запроса 10 секунд
|
||||
configuration.timeoutIntervalForResource = 3 // Таймаут ресурса 15 секунд
|
||||
self.customSession = URLSession(configuration: configuration)
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
}
|
||||
|
||||
@ -32,7 +37,7 @@ final class NetworkManager {
|
||||
func getSchedule(_ group: String) async throws -> Schedule {
|
||||
let newUrlForGroup = makeUrlForGroup(group)
|
||||
guard let url = URL(string: newUrlForGroup) else { throw NetworkError.invalidUrl }
|
||||
let (data, response) = try await URLSession.shared.data(from: url)
|
||||
let (data, response) = try await customSession.data(from: url)
|
||||
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse }
|
||||
|
||||
do {
|
||||
@ -47,7 +52,7 @@ final class NetworkManager {
|
||||
let newUrlForWeek = makeUrlForWeek(numOfWeek, htmlNameOfGroup)
|
||||
print(newUrlForWeek)
|
||||
guard let url = URL(string: newUrlForWeek) else { throw NetworkError.invalidUrl }
|
||||
let (data, response) = try await URLSession.shared.data(from: url)
|
||||
let (data, response) = try await customSession.data(from: url)
|
||||
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse }
|
||||
|
||||
do {
|
||||
@ -61,7 +66,7 @@ final class NetworkManager {
|
||||
func getGroups(group: String) async throws -> Welcome {
|
||||
let newUrlForGroups = makeUrlForGroup(group)
|
||||
guard let url = URL(string: newUrlForGroups) else { throw NetworkError.invalidUrl }
|
||||
let (data, response) = try await URLSession.shared.data(from: url)
|
||||
let (data, response) = try await customSession.data(from: url)
|
||||
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse }
|
||||
|
||||
do {
|
||||
|
37
Schedule ICTIS/Utilities/Network/NetworkMonitor.swift
Normal file
37
Schedule ICTIS/Utilities/Network/NetworkMonitor.swift
Normal file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// NetworkMonitor.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 27.03.2025.
|
||||
//
|
||||
|
||||
import Network
|
||||
import SwiftUI
|
||||
|
||||
class NetworkMonitor: ObservableObject {
|
||||
@Published var isConnected: Bool = false
|
||||
private let monitor = NWPathMonitor()
|
||||
private let queue = DispatchQueue(label: "NetworkMonitorQueue")
|
||||
|
||||
init() {
|
||||
startMonitoring()
|
||||
}
|
||||
|
||||
func startMonitoring() {
|
||||
monitor.pathUpdateHandler = { [weak self] path in
|
||||
DispatchQueue.main.async {
|
||||
self?.isConnected = path.status == .satisfied
|
||||
print(self?.isConnected == true ? "✅ Интернет подключен!" : "❌ Нет подключения к интернету.")
|
||||
}
|
||||
}
|
||||
monitor.start(queue: queue)
|
||||
}
|
||||
|
||||
func stopMonitoring() {
|
||||
monitor.cancel()
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopMonitoring()
|
||||
}
|
||||
}
|
@ -2,14 +2,14 @@
|
||||
// EditClassViewModel.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by G412 on 18.12.2024.
|
||||
// Created by Egor Mironov on 18.12.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
final class EditClassViewModel: ObservableObject {
|
||||
@Published var _class: ClassModel
|
||||
@Published var _class: CoreDataClassModel
|
||||
|
||||
let isNew: Bool
|
||||
|
||||
@ -17,7 +17,7 @@ final class EditClassViewModel: ObservableObject {
|
||||
|
||||
private let context: NSManagedObjectContext
|
||||
|
||||
init(provider: ClassProvider, _class: ClassModel? = nil) {
|
||||
init(provider: ClassProvider, _class: CoreDataClassModel? = nil) {
|
||||
self.provider = provider
|
||||
self.context = provider.newContext
|
||||
|
||||
@ -27,7 +27,7 @@ final class EditClassViewModel: ObservableObject {
|
||||
self.isNew = false
|
||||
}
|
||||
else {
|
||||
self._class = ClassModel(context: self.context)
|
||||
self._class = CoreDataClassModel(context: self.context)
|
||||
self.isNew = true
|
||||
}
|
||||
}
|
||||
|
35
Schedule ICTIS/ViewModel/SaveScheduleViewModel.swift
Normal file
35
Schedule ICTIS/ViewModel/SaveScheduleViewModel.swift
Normal file
@ -0,0 +1,35 @@
|
||||
//
|
||||
// SaveScheduleViewModel.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Egor Mironov on 02.04.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
final class SaveScheduleViewModel: ObservableObject {
|
||||
@Published var subject: JsonClassModel
|
||||
|
||||
|
||||
private let provider: ClassProvider
|
||||
|
||||
private let context: NSManagedObjectContext
|
||||
|
||||
init(provider: ClassProvider, subject: JsonClassModel? = nil) {
|
||||
self.provider = provider
|
||||
self.context = provider.newContext
|
||||
|
||||
if let subject,
|
||||
let existingClassCopy = provider.exists(subject, in: context) {
|
||||
self.subject = existingClassCopy
|
||||
}
|
||||
else {
|
||||
self.subject = JsonClassModel(context: self.context)
|
||||
}
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
try provider.persist(in: context)
|
||||
}
|
||||
}
|
@ -6,10 +6,17 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUICore
|
||||
|
||||
@MainActor
|
||||
final class ScheduleViewModel: ObservableObject {
|
||||
//MARK: Properties
|
||||
@Published var nameToHtml: [String : String] = [:]
|
||||
@Published var classesGroups: [[ClassInfo]] = []
|
||||
@Published var searchingGroup = ""
|
||||
@Published var filteringGroups: [String] = ["Все"]
|
||||
@Published var showOnlyChoosenGroup: String = "Все"
|
||||
|
||||
//Schedule
|
||||
@Published var weekScheduleGroup: Table = Table(
|
||||
type: "",
|
||||
@ -20,140 +27,89 @@ final class ScheduleViewModel: ObservableObject {
|
||||
link: ""
|
||||
)
|
||||
@Published var selectedDay: Date = .init()
|
||||
@Published var selectedIndex: Int = 1
|
||||
@Published var classes: [[String]] = []
|
||||
@Published var selectedIndex: Int = 0
|
||||
@Published var week: Int = 0
|
||||
@Published var numOfGroup: String = ""
|
||||
|
||||
@Published var isFirstStartOffApp = true
|
||||
@Published var isShowingAlertForIncorrectGroup: Bool = false
|
||||
@Published var errorInNetwork: NetworkError?
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var group: String = ""
|
||||
@Published var isNewGroup: Bool = false
|
||||
|
||||
//Groups
|
||||
@Published var groups: [Choice] = []
|
||||
//VPK
|
||||
@Published var vpks: [[String]] = []
|
||||
@Published var vpkHTML: String = ""
|
||||
@Published var vpk: String = ""
|
||||
@Published var weekScheduleVPK: Table = Table(
|
||||
type: "",
|
||||
name: "",
|
||||
week: 0,
|
||||
group: "",
|
||||
table: [[]],
|
||||
link: ""
|
||||
)
|
||||
|
||||
|
||||
//MARK: Methods
|
||||
func fetchWeekSchedule(group: String = "default", isOtherWeek: Bool = false) {
|
||||
//MARK: Methods
|
||||
func fetchWeekSchedule(isOtherWeek: Bool = false) {
|
||||
isLoading = true
|
||||
Task {
|
||||
do {
|
||||
var schedule: Schedule
|
||||
// В этот if мы заходим только если пользователь перелистывает недели и нам ИЗВЕСТНЫ номер группы(в html формате) и номер недели, которая показывается пользователю
|
||||
if (isOtherWeek || !isFirstStartOffApp) && (group == "default") {
|
||||
schedule = try await NetworkManager.shared.getScheduleForOtherWeek(self.week, self.numOfGroup)
|
||||
var updatedClassesGroups: [[ClassInfo]] = Array(repeating: [], count: 6) // 6 дней (пн-сб)
|
||||
|
||||
// Если другая неделя, запрашиваем расписание по неделе и номеру группу(в HTML формате)
|
||||
if isOtherWeek {
|
||||
let groupHTMLs = Array(self.nameToHtml.values)
|
||||
for groupHTML in groupHTMLs {
|
||||
let schedule = try await NetworkManager.shared.getScheduleForOtherWeek(self.week, groupHTML)
|
||||
let table = schedule.table.table
|
||||
let nameOfGroup = schedule.table.name
|
||||
|
||||
// Преобразуем данные в формат ClassInfo
|
||||
for (dayIndex, day) in table[2...].enumerated() { // Пропускаем первые две строки (заголовки)
|
||||
for (timeIndex, subject) in day.enumerated() {
|
||||
if !subject.isEmpty && timeIndex > 0 { // Пропускаем первый столбец (день и дату)
|
||||
let time = table[1][timeIndex] // Время берем из второй строки
|
||||
let classInfo = ClassInfo(subject: subject, group: nameOfGroup, time: time)
|
||||
updatedClassesGroups[dayIndex].append(classInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let groupNames = Array(self.nameToHtml.keys)
|
||||
for groupName in groupNames {
|
||||
let schedule = try await NetworkManager.shared.getSchedule(groupName)
|
||||
let numberHTML = schedule.table.group
|
||||
self.nameToHtml[groupName] = numberHTML
|
||||
let table = schedule.table.table
|
||||
self.week = schedule.table.week
|
||||
|
||||
// Преобразуем данные в формат ClassInfo
|
||||
for (dayIndex, day) in table[2...].enumerated() { // Пропускаем первые две строки (заголовки)
|
||||
for (timeIndex, subject) in day.enumerated() {
|
||||
if !subject.isEmpty && timeIndex > 0 { // Пропускаем первый столбец (день и дату)
|
||||
let time = table[1][timeIndex] // Время берем из второй строки
|
||||
let classInfo = ClassInfo(subject: subject, group: groupName, time: time)
|
||||
updatedClassesGroups[dayIndex].append(classInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// В else мы заходим в том случае, если НЕ знаем номер недели, которую нужно отобразить и номер группы(в html формате)
|
||||
else {
|
||||
print("Отладка 1")
|
||||
schedule = try await NetworkManager.shared.getSchedule(group)
|
||||
print("Отладка 2")
|
||||
self.group = group
|
||||
self.isNewGroup = true
|
||||
self.selectedDay = .init()
|
||||
}
|
||||
self.weekScheduleGroup = schedule.table
|
||||
self.week = weekScheduleGroup.week
|
||||
self.numOfGroup = weekScheduleGroup.group
|
||||
self.classes = weekScheduleGroup.table
|
||||
|
||||
// Обновляем данные
|
||||
self.classesGroups = updatedClassesGroups
|
||||
self.isFirstStartOffApp = false
|
||||
self.isShowingAlertForIncorrectGroup = false
|
||||
self.isLoading = false
|
||||
self.errorInNetwork = .noError
|
||||
print("Отладка 4")
|
||||
}
|
||||
catch {
|
||||
if let error = error as? NetworkError {
|
||||
switch (error) {
|
||||
case .invalidResponse:
|
||||
errorInNetwork = .invalidResponse
|
||||
case .invalidData:
|
||||
errorInNetwork = .invalidData
|
||||
self.isShowingAlertForIncorrectGroup = true
|
||||
default:
|
||||
print("Неизвестная ошибка: \(error)")
|
||||
}
|
||||
isLoading = false
|
||||
print("Есть ошибка: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchWeekVPK(isOtherWeek: Bool = false, vpk: String? = "default") {
|
||||
isLoading = true
|
||||
Task {
|
||||
do {
|
||||
var tempVPKS: Schedule
|
||||
// В этот if мы заходим только если пользователь перелистывает недели и нам известы номер ВПК(в html формате) и номер недели, которая показывается пользователю
|
||||
if isOtherWeek && vpk != nil {
|
||||
tempVPKS = try await NetworkManager.shared.getScheduleForOtherWeek(self.week, self.vpkHTML)
|
||||
}
|
||||
// В else мы заходим в том случае, если не знаем номер недели, которую нужно отобразить и номер группы(в html формате)
|
||||
else {
|
||||
tempVPKS = try await NetworkManager.shared.getSchedule(vpk!)
|
||||
self.vpk = vpk!
|
||||
self.selectedDay = .init()
|
||||
}
|
||||
self.weekScheduleVPK = tempVPKS.table
|
||||
self.vpkHTML = weekScheduleVPK.group
|
||||
self.vpks = weekScheduleVPK.table
|
||||
print(self.vpk)
|
||||
self.isShowingAlertForIncorrectGroup = false
|
||||
self.isLoading = false
|
||||
self.errorInNetwork = .noError
|
||||
}
|
||||
catch {
|
||||
if let error = error as? NetworkError {
|
||||
switch (error) {
|
||||
case .invalidResponse:
|
||||
errorInNetwork = .invalidResponse
|
||||
case .invalidData:
|
||||
errorInNetwork = .invalidData
|
||||
self.isShowingAlertForIncorrectGroup = true
|
||||
default:
|
||||
print("Неизвестная ошибка: \(error)")
|
||||
}
|
||||
isLoading = false
|
||||
print("Есть ошибка: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchGroups(group: String) {
|
||||
Task {
|
||||
do {
|
||||
var groups: Welcome
|
||||
groups = try await NetworkManager.shared.getGroups(group: group)
|
||||
self.groups = groups.choices
|
||||
|
||||
}
|
||||
catch {
|
||||
if let error = error as? NetworkError {
|
||||
switch (error) {
|
||||
// Сортируем по времени
|
||||
self.sortClassesByTime()
|
||||
} catch {
|
||||
if let urlError = error as? URLError, urlError.code == .timedOut {
|
||||
errorInNetwork = .timeout
|
||||
print("Ошибка: превышено время ожидания ответа от сервера")
|
||||
} else if let error = error as? NetworkError {
|
||||
switch error {
|
||||
case .invalidResponse:
|
||||
errorInNetwork = .invalidResponse
|
||||
case .invalidData:
|
||||
self.groups.removeAll()
|
||||
errorInNetwork = .invalidData
|
||||
self.isShowingAlertForIncorrectGroup = true
|
||||
default:
|
||||
self.groups.removeAll()
|
||||
print("Неизвестная ошибка: \(error)")
|
||||
}
|
||||
print("Есть ошибка: \(error)")
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -161,20 +117,97 @@ final class ScheduleViewModel: ObservableObject {
|
||||
func updateSelectedDayIndex() {
|
||||
switch selectedDay.format("E") {
|
||||
case "Пн":
|
||||
selectedIndex = 2
|
||||
selectedIndex = 0
|
||||
case "Вт":
|
||||
selectedIndex = 3
|
||||
selectedIndex = 1
|
||||
case "Ср":
|
||||
selectedIndex = 4
|
||||
selectedIndex = 2
|
||||
case "Чт":
|
||||
selectedIndex = 5
|
||||
selectedIndex = 3
|
||||
case "Пт":
|
||||
selectedIndex = 6
|
||||
selectedIndex = 4
|
||||
case "Сб":
|
||||
selectedIndex = 7
|
||||
selectedIndex = 5
|
||||
default:
|
||||
selectedIndex = 8
|
||||
selectedIndex = 6
|
||||
}
|
||||
}
|
||||
|
||||
private func parseTime(_ timeString: String) -> Int {
|
||||
// Разделяем строку по дефису и берем первую часть (время начала)
|
||||
let startTimeString = timeString.components(separatedBy: "-").first ?? ""
|
||||
|
||||
// Разделяем время на часы и минуты
|
||||
let components = startTimeString.components(separatedBy: ":")
|
||||
guard components.count == 2,
|
||||
let hours = Int(components[0]),
|
||||
let minutes = Int(components[1]) else {
|
||||
return 0 // В случае ошибки возвращаем 0
|
||||
}
|
||||
|
||||
// Преобразуем время в минуты с начала дня
|
||||
return hours * 60 + minutes
|
||||
}
|
||||
|
||||
// Method for sorting classes by time
|
||||
private func sortClassesByTime() {
|
||||
// Проходим по каждому дню (подмассиву) в classesGroups
|
||||
for dayIndex in 0..<classesGroups.count {
|
||||
// Сортируем подмассив по времени начала пары
|
||||
classesGroups[dayIndex].sort { class1, class2 in
|
||||
let time1 = parseTime(class1.time) // Время начала первой пары
|
||||
let time2 = parseTime(class2.time) // Время начала второй пары
|
||||
return time1 < time2 // Сортируем по возрастанию
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeFromSchedule(group: String) {
|
||||
self.nameToHtml[group] = nil
|
||||
|
||||
for i in classesGroups.indices {
|
||||
// Сначала находим индексы элементов для удаления
|
||||
let indicesToRemove = classesGroups[i].indices.filter { j in
|
||||
classesGroups[i][j].group.lowercased() == group.lowercased()
|
||||
}
|
||||
|
||||
// Удаляем элементы в обратном порядке, чтобы индексы оставались корректными
|
||||
for j in indicesToRemove.reversed() {
|
||||
classesGroups[i].remove(at: j)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateFilteringGroups() {
|
||||
self.filteringGroups = ["Все"]
|
||||
let keys = self.nameToHtml.keys
|
||||
self.filteringGroups.append(contentsOf: keys)
|
||||
}
|
||||
|
||||
func fillDictForVm() {
|
||||
let group1 = UserDefaults.standard.string(forKey: "group")
|
||||
let group2 = UserDefaults.standard.string(forKey: "group2")
|
||||
let group3 = UserDefaults.standard.string(forKey: "group3")
|
||||
let vpk1 = UserDefaults.standard.string(forKey: "vpk1")
|
||||
let vpk2 = UserDefaults.standard.string(forKey: "vpk2")
|
||||
let vpk3 = UserDefaults.standard.string(forKey: "vpk3")
|
||||
if let nameGroup1 = group1, nameGroup1 != "" {
|
||||
nameToHtml[nameGroup1] = ""
|
||||
}
|
||||
if let nameGroup2 = group2, nameGroup2 != "" {
|
||||
nameToHtml[nameGroup2] = ""
|
||||
}
|
||||
if let nameGroup3 = group3, nameGroup3 != "" {
|
||||
nameToHtml[nameGroup3] = ""
|
||||
}
|
||||
if let nameVpk1 = vpk1, nameVpk1 != "" {
|
||||
nameToHtml[nameVpk1] = ""
|
||||
}
|
||||
if let nameVpk2 = vpk2, nameVpk2 != "" {
|
||||
nameToHtml[nameVpk2] = ""
|
||||
}
|
||||
if let nameVpk3 = vpk3, nameVpk3 != "" {
|
||||
nameToHtml[nameVpk3] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
137
Schedule ICTIS/ViewModel/SearchGroupsViewModel.swift
Normal file
137
Schedule ICTIS/ViewModel/SearchGroupsViewModel.swift
Normal file
@ -0,0 +1,137 @@
|
||||
//
|
||||
// SearchGroupsViewModel.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by G412 on 06.03.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
final class SearchGroupsViewModel: ObservableObject {
|
||||
@Published var groups: [Subject] = []
|
||||
|
||||
func fetchGroups(group: String) {
|
||||
Task {
|
||||
do {
|
||||
var groups: Welcome
|
||||
groups = try await NetworkManager.shared.getGroups(group: group)
|
||||
self.groups = groups.choices
|
||||
if (group == "кт") {
|
||||
self.sortGroups()
|
||||
} else {
|
||||
self.sortVPK()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
if let error = error as? NetworkError {
|
||||
switch (error) {
|
||||
case .invalidData:
|
||||
self.groups.removeAll()
|
||||
default:
|
||||
self.groups.removeAll()
|
||||
print("Неизвестная ошибка: \(error)")
|
||||
}
|
||||
print("Есть ошибка: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Метод сортировки
|
||||
func sortGroups() {
|
||||
groups.sort { (group1, group2) in
|
||||
// Извлекаем компоненты из названия групп
|
||||
let components1 = extractComponents(from: group1.name)
|
||||
let components2 = extractComponents(from: group2.name)
|
||||
|
||||
// Сравниваем сначала по номеру курса (первая цифра после букв)
|
||||
if components1.courseNumber != components2.courseNumber {
|
||||
return components1.courseNumber < components2.courseNumber
|
||||
}
|
||||
|
||||
// Если номера курсов равны, сравниваем по номеру группы (число после дефиса)
|
||||
return components1.groupNumber < components2.groupNumber
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательная структура для хранения извлеченных компонентов
|
||||
private struct GroupComponents {
|
||||
let courseNumber: Int
|
||||
let groupNumber: Int
|
||||
}
|
||||
|
||||
// Метод для извлечения числовых компонентов из названия группы
|
||||
private func extractComponents(from name: String) -> GroupComponents {
|
||||
// Находим индекс дефиса
|
||||
guard let hyphenIndex = name.firstIndex(of: "-") else {
|
||||
return GroupComponents(courseNumber: 0, groupNumber: 0)
|
||||
}
|
||||
|
||||
// Извлекаем часть до дефиса (буквы и номер курса)
|
||||
let prefix = String(name[..<hyphenIndex])
|
||||
// Извлекаем часть после дефиса (номер группы)
|
||||
let suffix = String(name[name.index(after: hyphenIndex)...])
|
||||
|
||||
// Извлекаем цифры из prefix
|
||||
let courseNumberString = prefix.trimmingCharacters(in: CharacterSet.letters)
|
||||
let courseNumber = Int(courseNumberString) ?? 0
|
||||
|
||||
// Преобразуем suffix в число
|
||||
let groupNumber = Int(suffix) ?? 0
|
||||
|
||||
return GroupComponents(courseNumber: courseNumber, groupNumber: groupNumber)
|
||||
}
|
||||
|
||||
// Метод сортировки для ВПК групп
|
||||
func sortVPK() {
|
||||
groups.sort { (group1, group2) in
|
||||
let components1 = extractVPKComponents(from: group1.name)
|
||||
let components2 = extractVPKComponents(from: group2.name)
|
||||
|
||||
// Сравниваем по типу (мВПК после ВПК)
|
||||
if components1.isMinor != components2.isMinor {
|
||||
return !components1.isMinor // ВПК идет перед мВПК
|
||||
}
|
||||
|
||||
// Сравниваем по первому номеру
|
||||
if components1.firstNumber != components2.firstNumber {
|
||||
return components1.firstNumber < components2.firstNumber
|
||||
}
|
||||
|
||||
// Если первые номера равны, сравниваем по второму номеру (если есть)
|
||||
// Используем nil coalescing для случаев, когда второго номера нет
|
||||
return (components1.secondNumber ?? Int.max) < (components2.secondNumber ?? Int.max)
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательная структура для хранения компонентов ВПК
|
||||
private struct VPKComponents {
|
||||
let isMinor: Bool // true для мВПК, false для ВПК
|
||||
let firstNumber: Int
|
||||
let secondNumber: Int?
|
||||
}
|
||||
|
||||
// Метод для извлечения компонентов из названия ВПК группы
|
||||
private func extractVPKComponents(from name: String) -> VPKComponents {
|
||||
let isMinor = name.hasPrefix("мВПК")
|
||||
|
||||
// Убираем префикс и разбиваем по дефису
|
||||
let cleanName = isMinor ? name.replacingOccurrences(of: "мВПК-", with: "") : name.replacingOccurrences(of: "ВПК ", with: "")
|
||||
let components = cleanName.split(separator: "-")
|
||||
|
||||
// Извлекаем первый номер
|
||||
let firstNumberString = String(components[0]).trimmingCharacters(in: .whitespaces)
|
||||
let firstNumber = Int(firstNumberString) ?? 0
|
||||
|
||||
// Извлекаем второй номер, если он есть
|
||||
let secondNumber: Int?
|
||||
if components.count > 1 {
|
||||
secondNumber = Int(components[1]) ?? 0
|
||||
} else {
|
||||
secondNumber = nil
|
||||
}
|
||||
|
||||
return VPKComponents(isMinor: isMinor, firstNumber: firstNumber, secondNumber: secondNumber)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user