Initial Commit
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
||||
<entity name="CoreDataClassModel" representedClassName=".CoreDataClassModel" syncable="YES">
|
||||
<attribute name="auditory" optional="YES" attributeType="String"/>
|
||||
<attribute name="comment" optional="YES" attributeType="String"/>
|
||||
<attribute name="day" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="endtime" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="important" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="notification" optional="YES" attributeType="String"/>
|
||||
<attribute name="online" optional="YES" attributeType="String"/>
|
||||
<attribute name="professor" optional="YES" attributeType="String"/>
|
||||
<attribute name="starttime" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="subject" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="FavouriteGroupModel" representedClassName=".FavouriteGroupModel" syncable="YES">
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="FavouriteVpkModel" representedClassName=".FavouriteVpkModel" syncable="YES">
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="JsonClassModel" representedClassName=".JsonClassModel" syncable="YES">
|
||||
<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>
|
7
Schedule-ICTIS.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>Schedule-ICTIS.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
6
Schedule-ICTIS/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
74
Schedule-ICTIS/ContentView.swift
Normal file
@ -0,0 +1,74 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 13.11.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var selectedTab: TabBarModel = .schedule
|
||||
@State private var isTabBarHidden = false
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
@ObservedObject var networkMonitor: NetworkMonitor
|
||||
var body: some View {
|
||||
ZStack (alignment: .bottom) {
|
||||
TabView(selection: $selectedTab) {
|
||||
Text("Tasks")
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
@ -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: "Восстановите подключение к интернету чтобы мы смогли загрузить расписание")
|
||||
}
|
BIN
Schedule-ICTIS/Fonts/Montserrat-Black.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-BlackItalic.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-Bold.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-BoldItalic.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-ExtraBold.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-ExtraBoldItalic.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-ExtraLight.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-ExtraLightItalic.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-Italic.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-Light.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-LightItalic.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-Medium.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-MediumItalic.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-Regular.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-SemiBold.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-SemiBoldItalic.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-Thin.ttf
Executable file
BIN
Schedule-ICTIS/Fonts/Montserrat-ThinItalic.ttf
Executable file
27
Schedule-ICTIS/Info.plist
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Montserrat-Black.ttf</string>
|
||||
<string>Montserrat-BlackItalic.ttf</string>
|
||||
<string>Montserrat-Bold.ttf</string>
|
||||
<string>Montserrat-BoldItalic.ttf</string>
|
||||
<string>Monsterrat-ExtraBold.ttf</string>
|
||||
<string>Montserrat-ExtraBoldItalic.ttf</string>
|
||||
<string>Montserrat-ExtraLight.ttf</string>
|
||||
<string>Montserrat-ExtraLightItalic.ttf</string>
|
||||
<string>Montserrat-Italic.ttf</string>
|
||||
<string>Montserrat-Light.ttf</string>
|
||||
<string>Montserrat-LightItalic.ttf</string>
|
||||
<string>Montserrat-Medium.ttf</string>
|
||||
<string>Montserrat-MediumItalic.ttf</string>
|
||||
<string>Montserrat-Regular.ttf</string>
|
||||
<string>Montserrat-SemiBold.ttf</string>
|
||||
<string>Montserrat-SemiBoldItalic.ttf</string>
|
||||
<string>Montserrat-Thin.ttf</string>
|
||||
<string>Montserrat-ThinItalic.ttf</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
48
Schedule-ICTIS/Launch Screen.storyboard
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="obG-Y5-kRd">
|
||||
<rect key="frame" x="0.0" y="832" width="393" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" misplaced="YES" text="Schedule ICTIS" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
|
||||
<rect key="frame" x="0.0" y="405" width="393" height="43"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
||||
<color key="textColor" red="0.18870653208708893" green="0.40438775145842537" blue="0.96611279249191284" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="obG-Y5-kRd" secondAttribute="centerX" id="5cz-MP-9tL"/>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
|
||||
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="SfN-ll-jLj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="obG-Y5-kRd" secondAttribute="bottom" constant="20" id="Y44-ml-fuU"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/3" constant="1" id="moa-c2-u7t"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="x7j-FC-K8j"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="52.671755725190835" y="374.64788732394368"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
32
Schedule-ICTIS/LoadingViews/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()
|
||||
}
|
53
Schedule-ICTIS/LoadingViews/LoadingScheduleView.swift
Normal file
@ -0,0 +1,53 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LoadingScheduleView: View {
|
||||
@State private var isAnimated = false
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 20) {
|
||||
ForEach(0..<5, id: \.self) { _ in
|
||||
VStack (alignment: .trailing) {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
isAnimated ? Color.gray.opacity(0.6) : Color.gray.opacity(0.3),
|
||||
isAnimated ? Color.gray.opacity(0.3) : Color.gray.opacity(0.6)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 45, height: 20)
|
||||
.padding(.horizontal, 20)
|
||||
.animation(.linear(duration: 0.8).repeatForever(autoreverses: true), value: isAnimated)
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
isAnimated ? Color.gray.opacity(0.6) : Color.gray.opacity(0.3),
|
||||
isAnimated ? Color.gray.opacity(0.3) : Color.gray.opacity(0.6)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(height: 70)
|
||||
.padding(.horizontal, 20)
|
||||
.animation(.linear(duration: 0.8).repeatForever(autoreverses: true), value: isAnimated)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isAnimated.toggle()
|
||||
}
|
||||
.padding(.top, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LoadingScheduleView()
|
||||
}
|
29
Schedule-ICTIS/LoadingViews/LoadingView.swift
Normal file
@ -0,0 +1,29 @@
|
||||
//
|
||||
// LoadingView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 04.04.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoadingView: View {
|
||||
@State private var isAnimating = false
|
||||
|
||||
var body: some View {
|
||||
Circle()
|
||||
.trim(from: 0.2, to: 1.0)
|
||||
.stroke(Color("blueColor"), lineWidth: 3)
|
||||
.frame(width: 30, height: 30)
|
||||
.rotationEffect(Angle(degrees: isAnimating ? 360 : 0))
|
||||
.animation(
|
||||
Animation.linear(duration: 0.6).repeatForever(autoreverses: false),
|
||||
value: isAnimating
|
||||
)
|
||||
.onAppear { isAnimating = true }
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LoadingView()
|
||||
}
|
52
Schedule-ICTIS/Main/Views/CreatedClassView.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// CreatedClassView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 23.12.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CreatedClassView: View {
|
||||
@ObservedObject var _class: CoreDataClassModel
|
||||
var provider = ClassProvider.shared
|
||||
var body: some View {
|
||||
let existingCopy = try? provider.viewContext.existingObject(with: _class.objectID)
|
||||
if existingCopy != nil {
|
||||
HStack(spacing: 15) {
|
||||
VStack {
|
||||
Text(getTimeString(_class.starttime))
|
||||
.font(.custom("Montserrat-Regular", fixedSize: 15))
|
||||
.padding(.bottom, 1)
|
||||
Text(getTimeString(_class.endtime))
|
||||
.font(.custom("Montserrat-Regular", fixedSize: 15))
|
||||
.padding(.top, 1)
|
||||
}
|
||||
.frame(width: 48)
|
||||
.padding(.top, 7)
|
||||
.padding(.bottom, 7)
|
||||
.padding(.leading, 10)
|
||||
Rectangle()
|
||||
.frame(width: 2)
|
||||
.frame(maxHeight: UIScreen.main.bounds.height - 18)
|
||||
.padding(.top, 7)
|
||||
.padding(.bottom, 7)
|
||||
.foregroundColor(_class.important ? Color("redForImportant") : onlineOrNot(_class.online))
|
||||
Text(getSubjectName(_class.subject, _class.professor, _class.auditory))
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 15))
|
||||
.lineSpacing(3)
|
||||
.padding(.top, 9)
|
||||
.padding(.bottom, 9)
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: UIScreen.main.bounds.width - 40, maxHeight: 230)
|
||||
.background(Color.white)
|
||||
.cornerRadius(20)
|
||||
.shadow(color: .black.opacity(0.25), radius: 4, x: 2, y: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CreatedClassView(_class: .preview())
|
||||
}
|
47
Schedule-ICTIS/Main/Views/Fields/AuditoryFieldView.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// AuditoryFieldView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by G412 on 23.01.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AuditoryFieldView: View {
|
||||
@Binding var text: String
|
||||
var labelForField: String
|
||||
@FocusState var isFocused: Bool
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.foregroundColor(Color.gray)
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing, 14)
|
||||
TextField(labelForField, text: $text)
|
||||
.font(.custom("Montserrat-Meduim", fixedSize: 17))
|
||||
.disableAutocorrection(true)
|
||||
.submitLabel(.done)
|
||||
.focused($isFocused)
|
||||
if isFocused {
|
||||
Button {
|
||||
self.text = ""
|
||||
self.isFocused = false
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.padding(.trailing, 20)
|
||||
.offset(x: 10)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 40)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AuditoryFieldView(text: .constant(""), labelForField: "Корпус-аудитория")
|
||||
}
|
44
Schedule-ICTIS/Main/Views/Fields/CommentFieldView.swift
Normal file
@ -0,0 +1,44 @@
|
||||
//
|
||||
// CommentView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by G412 on 17.12.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CommentFieldView: View {
|
||||
@Binding var textForComment: String
|
||||
@FocusState var isFocused: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField("Комментарий", text: $textForComment)
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.submitLabel(.done)
|
||||
.multilineTextAlignment(.leading)
|
||||
.focused($isFocused)
|
||||
.padding(.top, 6)
|
||||
.padding(.bottom, 6)
|
||||
|
||||
if isFocused {
|
||||
Button {
|
||||
textForComment = ""
|
||||
self.isFocused = false
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.padding(.trailing, 20)
|
||||
.offset(x: 10)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 40)
|
||||
.padding(.horizontal)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
47
Schedule-ICTIS/Main/Views/Fields/ProfessorFieldView.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// ProfessorFieldView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by G412 on 23.01.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ProfessorFieldView: View {
|
||||
@Binding var text: String
|
||||
var labelForField: String
|
||||
@FocusState var isFocused: Bool
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "graduationcap")
|
||||
.foregroundColor(Color.gray)
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing, 7)
|
||||
TextField(labelForField, text: $text)
|
||||
.font(.custom("Montserrat-Meduim", fixedSize: 17))
|
||||
.disableAutocorrection(true)
|
||||
.submitLabel(.done)
|
||||
.focused($isFocused)
|
||||
if isFocused {
|
||||
Button {
|
||||
self.text = ""
|
||||
self.isFocused = false
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.padding(.trailing, 20)
|
||||
.offset(x: 10)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 40)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ProfessorFieldView(text: .constant(""), labelForField: "Преподаватель")
|
||||
}
|
61
Schedule-ICTIS/Main/Views/Fields/StartEndTimeFieldView.swift
Normal file
@ -0,0 +1,61 @@
|
||||
//
|
||||
// StartEndTimeView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 17.12.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StartEndTimeFieldView: View {
|
||||
@Binding var isIncorrectDate: Bool
|
||||
@Binding var selectedDay: Date
|
||||
@Binding var selectedTime: Date
|
||||
var imageName: String
|
||||
var text: String
|
||||
@Binding var isTimeSelected: Bool
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: imageName)
|
||||
.foregroundColor(isIncorrectDate ? .red : Color("grayForFields"))
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing, 5)
|
||||
|
||||
if !isTimeSelected || isIncorrectDate {
|
||||
Text(text)
|
||||
.font(.custom("Montserrat-Meduim", fixedSize: 17))
|
||||
.foregroundColor(.gray.opacity(0.5))
|
||||
}
|
||||
else {
|
||||
Text("\(selectedTime, formatter: timeFormatter)")
|
||||
.foregroundColor(isIncorrectDate ? .red : .black)
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: (UIScreen.main.bounds.width / 2) - 22, height: 40)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
.overlay {
|
||||
if selectedDay.isToday {
|
||||
DatePicker("", selection: $selectedTime, in: Date()..., displayedComponents: .hourAndMinute)
|
||||
.padding(.trailing, 35)
|
||||
.blendMode(.destinationOver)
|
||||
.onChange(of: selectedTime) { newValue, oldValue in
|
||||
isTimeSelected = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
DatePicker("", selection: $selectedTime, displayedComponents: .hourAndMinute)
|
||||
.padding(.trailing, 35)
|
||||
.blendMode(.destinationOver)
|
||||
.onChange(of: selectedTime) { newValue, oldValue in
|
||||
isTimeSelected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
Schedule-ICTIS/Main/Views/Fields/SubjectFieldView.swift
Normal file
@ -0,0 +1,65 @@
|
||||
//
|
||||
// Field.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by G412 on 16.12.2024.
|
||||
// КТбо2-6
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SubjectFieldView: View {
|
||||
@Binding var text: String
|
||||
@Binding var isShowingSubjectFieldRed: Bool
|
||||
@Binding var labelForField: String
|
||||
@FocusState var isFocused: Bool
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "book")
|
||||
.foregroundColor(Color.gray)
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing, 9)
|
||||
TextField(labelForField, text: $text)
|
||||
.font(.custom("Montserrat-Meduim", fixedSize: 17))
|
||||
.disableAutocorrection(true)
|
||||
.submitLabel(.done)
|
||||
.focused($isFocused)
|
||||
.onChange(of: isFocused, initial: false) { oldValue, newValue in
|
||||
if newValue {
|
||||
self.isShowingSubjectFieldRed = false
|
||||
self.labelForField = "Предмет"
|
||||
}
|
||||
}
|
||||
.background {
|
||||
Group {
|
||||
if isShowingSubjectFieldRed {
|
||||
Text("Поле должно быть заполнено!")
|
||||
.font(.custom("Montserrat-Meduim", fixedSize: 17))
|
||||
.foregroundColor(.red)
|
||||
.frame(width: 290)
|
||||
.padding(.leading, -38)
|
||||
}
|
||||
}
|
||||
}
|
||||
if isFocused {
|
||||
Button {
|
||||
self.text = ""
|
||||
self.isFocused = false
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.padding(.trailing, 20)
|
||||
.offset(x: 10)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 40)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SubjectFieldView(text: .constant(""), isShowingSubjectFieldRed: .constant(false), labelForField: .constant("Предмет"))
|
||||
}
|
51
Schedule-ICTIS/Main/Views/FilterGroupsView.swift
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// 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) {
|
||||
LazyHStack(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)
|
||||
}
|
||||
.frame(height: 40)
|
||||
.onAppear {
|
||||
vm.updateFilteringGroups()
|
||||
}
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@Previewable @ObservedObject var vm = ScheduleViewModel()
|
||||
FilterGroupsView(vm: vm)
|
||||
}
|
20
Schedule-ICTIS/Main/Views/FirstLaunchScheduleView.swift
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// FirstLaunchScheduleView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 06.12.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct FirstLaunchScheduleView: View {
|
||||
var body: some View {
|
||||
VStack () {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
FirstLaunchScheduleView()
|
||||
}
|
96
Schedule-ICTIS/Main/Views/MainView.swift
Normal file
@ -0,0 +1,96 @@
|
||||
//
|
||||
// ScheduleView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 13.11.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MainView: View {
|
||||
@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(isFocused: _isFocusedSearchBar, vm: vm, isShowingMonthSlider: $isShowingMonthSlider)
|
||||
.onChange(of: isScrolling, initial: false) { oldValue, newValue in
|
||||
if newValue && isScrolling {
|
||||
isFocusedSearchBar = false
|
||||
}
|
||||
}
|
||||
CurrentDateView()
|
||||
FilterGroupsView(vm: vm)
|
||||
if vm.isLoading {
|
||||
LoadingScheduleView()
|
||||
}
|
||||
else {
|
||||
ScheduleView(vm: vm, networkMonitor: networkMonitor, isScrolling: $isScrolling)
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $vm.isShowingAlertForIncorrectGroup, error: vm.errorInNetwork) { error in
|
||||
Button("ОК") {
|
||||
print("This alert")
|
||||
vm.isShowingAlertForIncorrectGroup = false
|
||||
vm.errorInNetwork = nil
|
||||
}
|
||||
} message: { error in
|
||||
Text(error.failureReason)
|
||||
}
|
||||
.background(Color("background"))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func CurrentDateView() -> some View {
|
||||
VStack (alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
VStack (alignment: .leading, spacing: 0) {
|
||||
Text(vm.selectedDay.format("EEEE"))
|
||||
.font(.custom("Montserrat-SemiBold", fixedSize: 30))
|
||||
.foregroundStyle(.black)
|
||||
HStack (spacing: 5) {
|
||||
Text(vm.selectedDay.format("dd"))
|
||||
.font(.custom("Montserrat-Bold", fixedSize: 17))
|
||||
.foregroundStyle(Color("grayForDate"))
|
||||
Text(vm.selectedDay.format("MMMM"))
|
||||
.font(.custom("Montserrat-Bold", fixedSize: 17))
|
||||
.foregroundStyle(Color("grayForDate"))
|
||||
Spacer()
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
isShowingMonthSlider.toggle()
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 2) {
|
||||
Text(isShowingMonthSlider ? "Свернуть" : "Развернуть")
|
||||
.font(.custom("Montserrat-Regular", fixedSize: 15))
|
||||
.foregroundStyle(Color.blue)
|
||||
Image(isShowingMonthSlider ? "arrowup" : "arrowdown")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.leading, 5)
|
||||
Spacer()
|
||||
}
|
||||
if (!isShowingMonthSlider) {
|
||||
WeekTabView(vm: vm)
|
||||
.transition(.opacity)
|
||||
.animation(.easeInOut(duration: 0.25), value: isShowingMonthSlider)
|
||||
}
|
||||
else {
|
||||
MonthTabView(vm: vm)
|
||||
.transition(.opacity)
|
||||
.animation(.linear(duration: 0.5), value: isShowingMonthSlider)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
24
Schedule-ICTIS/Main/Views/NoScheduleView.swift
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// NoScheduleView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 12.12.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NoScheduleView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollView (showsIndicators: false) {
|
||||
Text("Расслабся братан, расписания еще нет")
|
||||
.padding(.top, 100)
|
||||
.font(.custom("Montserrat-SemiBold", fixedSize: 17))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NoScheduleView()
|
||||
}
|
170
Schedule-ICTIS/Main/Views/ScheduleView.swift
Normal file
@ -0,0 +1,170 @@
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct ScheduleView: View {
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
@ObservedObject var networkMonitor: NetworkMonitor
|
||||
@FetchRequest(fetchRequest: CoreDataClassModel.all()) var classes // Список пар добавленных пользователем
|
||||
@FetchRequest(fetchRequest: JsonClassModel.all()) private var subjects // Список пар сохраненных в CoreData(для отсутствия интернета)
|
||||
@FetchRequest(fetchRequest: FavouriteGroupModel.all()) private var favGroups
|
||||
@FetchRequest(fetchRequest: FavouriteVpkModel.all()) private var favVpk
|
||||
@State private var selectedClass: CoreDataClassModel? = nil
|
||||
@State private var lastOffset: CGFloat = 0
|
||||
@State private var scrollTimer: Timer? = nil
|
||||
@Binding var isScrolling: Bool
|
||||
var provider = ClassProvider.shared
|
||||
|
||||
private var hasSubjectsToShow: Bool {
|
||||
subjects.contains { subject in
|
||||
subject.week == vm.week
|
||||
}
|
||||
}
|
||||
|
||||
private var hasClassesToShow: Bool {
|
||||
classes.contains { _class in
|
||||
_class.day == vm.selectedDay
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
if networkMonitor.isConnected {
|
||||
onlineContent
|
||||
} else {
|
||||
offlineContent
|
||||
}
|
||||
gradientOverlay
|
||||
}
|
||||
.onAppear {
|
||||
deleteClassesFormCoreDataIfMonday()
|
||||
if networkMonitor.isConnected {
|
||||
checkSavingOncePerDay()
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedClass, onDismiss: { selectedClass = nil }) { _class in
|
||||
CreateEditClassView(vm: .init(provider: provider, _class: _class), day: vm.selectedDay)
|
||||
}
|
||||
}
|
||||
|
||||
private var onlineContent: some View {
|
||||
Group {
|
||||
if vm.errorInNetwork == .timeout {
|
||||
NetworkErrorView(message: "Проверьте подключение к интернету")
|
||||
} else if vm.errorInNetwork == .invalidResponse {
|
||||
NoScheduleView()
|
||||
}
|
||||
else if vm.errorInNetwork == .noError {
|
||||
scheduleScrollView(isOnline: true)
|
||||
}
|
||||
else {
|
||||
NoScheduleView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if vm.classesGroups.isEmpty {
|
||||
vm.fetchWeekSchedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var offlineContent: some View {
|
||||
scheduleScrollView(isOnline: false)
|
||||
}
|
||||
|
||||
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) && !hasSubjectsToShow {
|
||||
ConnectingToNetworkView()
|
||||
.padding(.top, 100)
|
||||
} else {
|
||||
ForEach(filteredSubjects, id: \.self) { subject in
|
||||
if vm.showOnlyChoosenGroup == "Все" || subject.group == vm.showOnlyChoosenGroup {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
struct ViewOffsetKey: PreferenceKey {
|
||||
typealias Value = CGFloat
|
||||
static var defaultValue = CGFloat.zero
|
||||
static func reduce(value: inout Value, nextValue: () -> Value) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
99
Schedule-ICTIS/Main/Views/SearchBarView.swift
Normal file
@ -0,0 +1,99 @@
|
||||
//
|
||||
// SearchBarView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 13.11.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchBarView: View {
|
||||
@State private var text: String = ""
|
||||
@FocusState var isFocused: Bool
|
||||
@State private var isShowingSheet: Bool = false
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
@Binding var isShowingMonthSlider: Bool
|
||||
|
||||
var provider = ClassProvider.shared
|
||||
|
||||
var body: some View {
|
||||
HStack (spacing: 11) {
|
||||
HStack (spacing: 0) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(Color.gray)
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing, 7)
|
||||
TextField("Поиск группы", text: $text)
|
||||
.disableAutocorrection(true)
|
||||
.focused($isFocused)
|
||||
.onSubmit {
|
||||
self.isFocused = false
|
||||
if (!text.isEmpty) {
|
||||
vm.fetchWeekForSingleGroup(groupName: text)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
guard vm.errorInNetwork == .noError else {
|
||||
vm.isShowingAlertForIncorrectGroup = true
|
||||
return
|
||||
}
|
||||
vm.removeFromSchedule(group: vm.searchingGroup)
|
||||
text = transformStringToFormat(text)
|
||||
vm.searchingGroup = text
|
||||
vm.nameToHtml[text] = ""
|
||||
print("Ключи: \(vm.nameToHtml.keys)")
|
||||
vm.updateFilteringGroups()
|
||||
vm.fetchWeekSchedule()
|
||||
self.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
.submitLabel(.search)
|
||||
if isFocused {
|
||||
Button {
|
||||
self.text = ""
|
||||
self.isFocused = false
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.padding(.trailing, 20)
|
||||
.offset(x: 10)
|
||||
.foregroundColor(.gray)
|
||||
.background(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
self.isShowingMonthSlider = false
|
||||
})
|
||||
.frame(height: 40)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
if !isFocused {
|
||||
Button {
|
||||
isShowingSheet = true
|
||||
} label: {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundStyle(Color("blueColor"))
|
||||
.cornerRadius(15)
|
||||
Image(systemName: "plus")
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.scaledToFit()
|
||||
.frame(width: 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 5)
|
||||
.frame(height: 40)
|
||||
.accentColor(.blue)
|
||||
.sheet(isPresented: $isShowingSheet) {
|
||||
CreateEditClassView(vm: .init(provider: provider), day: vm.selectedDay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
301
Schedule-ICTIS/Main/Views/Sheets/CreateEditClassView.swift
Normal file
@ -0,0 +1,301 @@
|
||||
//
|
||||
// SheetCreateClassView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 12.12.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CreateEditClassView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var isShowingDatePickerForDate: Bool = false
|
||||
@ObservedObject var vm: EditClassViewModel
|
||||
var day: Date
|
||||
@State private var isIncorrectDate1: Bool = false
|
||||
@State private var isIncorrectDate2: Bool = false
|
||||
@State private var isShowingSubjectFieldRed: Bool = false
|
||||
@State private var isSelectedTime1 = false
|
||||
@State private var isSelectedTime2 = false
|
||||
@State private var textForLabelInSubjectField: String = "Предмет"
|
||||
@State private var selectedType: String = "Оффлайн"
|
||||
@FocusState private var isFocusedSubject: Bool
|
||||
@FocusState private var isFocusedAuditory: Bool
|
||||
@FocusState private var isFocusedProfessor: Bool
|
||||
@FocusState private var isFocusedComment: Bool
|
||||
var provider = ClassProvider.shared
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack {
|
||||
SubjectFieldView(text: $vm._class.subject, isShowingSubjectFieldRed: $isShowingSubjectFieldRed, labelForField: $textForLabelInSubjectField, isFocused: _isFocusedSubject)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
HStack {
|
||||
HStack {
|
||||
Text("Тип")
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.foregroundColor(.black)
|
||||
Spacer()
|
||||
HStack {
|
||||
Text(vm._class.online)
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.foregroundColor(Color("customGray3"))
|
||||
Image("upDownArrows")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
.overlay {
|
||||
HStack {
|
||||
Spacer()
|
||||
Picker("Тип", selection: $vm._class.online, content: {
|
||||
ForEach(MockData.onlineOrOffline, id: \.self) {
|
||||
Text($0)
|
||||
}
|
||||
})
|
||||
.accentColor(Color("grayForFields"))
|
||||
.padding(.trailing, 35)
|
||||
.blendMode(.destinationOver)
|
||||
}
|
||||
.frame(width: UIScreen.main.bounds.width)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
|
||||
ZStack {
|
||||
if vm._class.online == "Оффлайн" {
|
||||
AuditoryFieldView(text: $vm._class.auditory, labelForField: "Корпус-аудитория", isFocused: _isFocusedAuditory)
|
||||
.padding(.bottom, 10)
|
||||
.transition(.asymmetric(
|
||||
insertion: .offset(y: -50).combined(with: .identity),
|
||||
removal: .offset(y: -50).combined(with: .opacity)
|
||||
))
|
||||
}
|
||||
}
|
||||
.animation(
|
||||
vm._class.online == "Оффлайн" ?
|
||||
.linear(duration: 0.3) : // Анимация для появления
|
||||
.linear(duration: 0.2), // Анимация для исчезновения
|
||||
value: vm._class.online
|
||||
)
|
||||
|
||||
ProfessorFieldView(text: $vm._class.professor, labelForField: "Преподаватель", isFocused: _isFocusedProfessor)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
HStack {
|
||||
Image(systemName: "calendar")
|
||||
.foregroundColor(Color.gray)
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing, 5)
|
||||
Text("Дата")
|
||||
.foregroundColor(Color("grayForFields").opacity(0.5))
|
||||
.font(.custom("Montserrat-Meduim", fixedSize: 17))
|
||||
Spacer()
|
||||
Text("\(vm._class.day, formatter: dateFormatter)")
|
||||
.foregroundColor(.black)
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.padding(.trailing, 20)
|
||||
}
|
||||
.frame(height: 40)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
.overlay {
|
||||
DatePicker("", selection: $vm._class.day, in: Date()..., displayedComponents: .date)
|
||||
.blendMode(.destinationOver)
|
||||
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
HStack {
|
||||
StartEndTimeFieldView(isIncorrectDate: $isIncorrectDate1, selectedDay: $vm._class.day, selectedTime: $vm._class.starttime, imageName: "clock", text: "Начало", isTimeSelected: $isSelectedTime1)
|
||||
.onChange(of: vm._class.starttime) { oldValue, newValue in
|
||||
if !checkStartTimeLessThenEndTime(vm._class.starttime, vm._class.endtime) {
|
||||
self.isIncorrectDate1 = true
|
||||
self.isSelectedTime1 = false
|
||||
print("Первый")
|
||||
print(self.isSelectedTime1)
|
||||
print(self.isSelectedTime2)
|
||||
}
|
||||
else {
|
||||
self.isIncorrectDate1 = false
|
||||
self.isIncorrectDate2 = false
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
StartEndTimeFieldView(isIncorrectDate: $isIncorrectDate2, selectedDay: $vm._class.day, selectedTime: $vm._class.endtime, imageName: "clock.badge.xmark", text: "Конец", isTimeSelected: $isSelectedTime2)
|
||||
.onChange(of: vm._class.endtime) { oldValue, newValue in
|
||||
if !checkStartTimeLessThenEndTime(vm._class.starttime, vm._class.endtime) {
|
||||
self.isIncorrectDate2 = true
|
||||
self.isSelectedTime2 = false
|
||||
print("Второй")
|
||||
print(self.isSelectedTime1)
|
||||
print(self.isSelectedTime2)
|
||||
}
|
||||
else {
|
||||
self.isIncorrectDate1 = false
|
||||
self.isIncorrectDate2 = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 40)
|
||||
.padding(.bottom, 10)
|
||||
Toggle("Пометить как важную", isOn: $vm._class.important)
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.frame(height: 40)
|
||||
.padding(.horizontal)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
HStack {
|
||||
HStack {
|
||||
Text("Напоминание")
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.foregroundColor(.black)
|
||||
Spacer()
|
||||
HStack {
|
||||
Text(vm._class.notification)
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
.foregroundColor(Color("customGray3"))
|
||||
Image("upDownArrows")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
.overlay {
|
||||
HStack {
|
||||
Spacer()
|
||||
Picker("", selection: $vm._class.notification , content: {
|
||||
ForEach(MockData.notifications, id: \.self) {
|
||||
Text($0)
|
||||
}
|
||||
})
|
||||
.accentColor(Color("grayForFields"))
|
||||
.padding(.trailing, 35)
|
||||
.blendMode(.destinationOver)
|
||||
}
|
||||
.frame(width: UIScreen.main.bounds.width)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
|
||||
CommentFieldView(textForComment: $vm._class.comment, isFocused: _isFocusedComment)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
|
||||
if !vm.isNew {
|
||||
Button {
|
||||
do {
|
||||
try provider.delete(vm._class, in: provider.viewContext)
|
||||
dismiss()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: "trash")
|
||||
Text("Удалить занятие")
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
Spacer()
|
||||
}
|
||||
.frame(height: 40)
|
||||
.background(Color.white)
|
||||
.foregroundColor(Color.red)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 60)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Отменить") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Сохранить") {
|
||||
isFocusedSubject = false
|
||||
isFocusedProfessor = false
|
||||
isFocusedAuditory = false
|
||||
isFocusedComment = false
|
||||
if (vm._class.subject.isEmpty || (isIncorrectDate1 || isIncorrectDate2) || (!isSelectedTime1 || !isSelectedTime2)) {
|
||||
if (vm._class.subject.isEmpty) {
|
||||
self.isShowingSubjectFieldRed = true
|
||||
self.textForLabelInSubjectField = ""
|
||||
}
|
||||
if !isSelectedTime1 {
|
||||
self.isIncorrectDate1 = true
|
||||
}
|
||||
if !isSelectedTime2 {
|
||||
self.isIncorrectDate2 = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
do {
|
||||
try vm.save()
|
||||
dismiss()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(vm.isNew ? "Новая пара" : "Изменить данные")
|
||||
.background(Color("background"))
|
||||
.onAppear {
|
||||
let temp = Calendar.current.date(byAdding: .hour, value: 1, to: Date.init())
|
||||
if let endTime = temp {
|
||||
if (!hoursMinutesAreEqual(date1: vm._class.starttime, isEqualTo: Date()) && !hoursMinutesAreEqual(date1: vm._class.endtime, isEqualTo: endTime)) {
|
||||
self.isSelectedTime1 = true
|
||||
self.isSelectedTime2 = true
|
||||
print(vm._class.starttime)
|
||||
print(vm._class.endtime)
|
||||
print(endTime)
|
||||
print(Date())
|
||||
}
|
||||
}
|
||||
if day > Calendar.current.startOfDay(for: Date()) {
|
||||
vm._class.day = day
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
isFocusedSubject = false
|
||||
isFocusedProfessor = false
|
||||
isFocusedAuditory = false
|
||||
isFocusedComment = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let day: Date = .init()
|
||||
CreateEditClassView(vm: .init(provider: .shared), day: day)
|
||||
}
|
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])
|
||||
}
|
||||
}
|
||||
}
|
84
Schedule-ICTIS/Main/Views/TabViews/MonthTabView.swift
Normal file
@ -0,0 +1,84 @@
|
||||
//
|
||||
// MonthTabView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 10.12.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MonthTabView: View {
|
||||
@State var currentMonthIndex: Int = 1
|
||||
@State var monthSlider: [[Date.MonthWeek]] = []
|
||||
@State private var createMonth: Bool = false
|
||||
@State private var currentWeekIndex: Int = 0
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack (spacing: 33) {
|
||||
ForEach(MockData.daysOfWeek.indices, id: \.self) { index in
|
||||
Text(MockData.daysOfWeek[index])
|
||||
.font(.custom("Montserrat-SemiBold", fixedSize: 15))
|
||||
.foregroundColor(MockData.daysOfWeek[index] == "Вс" ? Color(.red) : Color("customGray2"))
|
||||
}
|
||||
}
|
||||
TabView(selection: $currentMonthIndex) {
|
||||
ForEach(monthSlider.indices, id: \.self) { index in
|
||||
let month = monthSlider[index]
|
||||
MonthView(month)
|
||||
.tag(index)
|
||||
.transition(.slide)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, -15)
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
//.animation(.easeIn(duration: 0.3), value: currentMonthIndex)
|
||||
}
|
||||
.frame(height: 220)
|
||||
.padding(.top, 26)
|
||||
.padding(.bottom, 20)
|
||||
.onAppear {
|
||||
updateMonthScreenViewForNewGroup()
|
||||
}
|
||||
.onChange(of: currentMonthIndex, initial: false) { oldValue, newValue in
|
||||
if newValue == 0 || newValue == (monthSlider.count - 1) {
|
||||
createMonth = true
|
||||
}
|
||||
}
|
||||
.onChange(of: vm.isNewGroup, initial: false) { oldValue, newValue in
|
||||
if newValue {
|
||||
monthSlider.removeAll()
|
||||
currentMonthIndex = 1
|
||||
updateMonthScreenViewForNewGroup()
|
||||
vm.isNewGroup = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func MonthView(_ month: [Date.MonthWeek]) -> some View {
|
||||
VStack (spacing: 10) {
|
||||
ForEach(month.indices, id: \.self) { index in
|
||||
let week = month[index].week
|
||||
WeekViewForMonth(week: week, vm: vm)
|
||||
}
|
||||
}
|
||||
.background {
|
||||
GeometryReader {
|
||||
let minX = $0.frame(in: .global).minX
|
||||
|
||||
Color.clear
|
||||
.preference(key: OffsetKey.self, value: minX)
|
||||
.onPreferenceChange(OffsetKey.self) { value in
|
||||
if (abs(value.rounded()) - 20) < 5 && createMonth {
|
||||
paginateMonth()
|
||||
|
||||
createMonth = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
46
Schedule-ICTIS/Main/Views/TabViews/WeekTabView.swift
Normal file
@ -0,0 +1,46 @@
|
||||
//
|
||||
// WeekTabView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 10.12.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WeekTabView: View {
|
||||
@State private var currentWeekIndex: Int = 1
|
||||
@State var weekSlider: [[Date.WeekDay]] = []
|
||||
@State private var createWeek: Bool = false
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
var body: some View {
|
||||
HStack {
|
||||
TabView(selection: $currentWeekIndex) {
|
||||
ForEach(weekSlider.indices, id: \.self) { index in
|
||||
let week = weekSlider[index]
|
||||
WeekViewForWeek(weekSlider: $weekSlider, currentWeekIndex: $currentWeekIndex, createWeek: $createWeek, week: week, vm: vm)
|
||||
.padding(.horizontal, 15)
|
||||
.tag(index)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, -15)
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
.frame(height: 90)
|
||||
}
|
||||
.onAppear(perform: {
|
||||
updateWeekScreenViewForNewGroup()
|
||||
})
|
||||
.onChange(of: currentWeekIndex, initial: false) { oldValue, newValue in
|
||||
if newValue == 0 || newValue == (weekSlider.count - 1) {
|
||||
createWeek = true
|
||||
}
|
||||
}
|
||||
.onChange(of: vm.isNewGroup, initial: false) { oldValue, newValue in
|
||||
if newValue {
|
||||
weekSlider.removeAll()
|
||||
currentWeekIndex = 1
|
||||
updateWeekScreenViewForNewGroup()
|
||||
vm.isNewGroup = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
Schedule-ICTIS/Main/Views/TabViews/WeekViewForMonth.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// WeekViewForMonth.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 20.12.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WeekViewForMonth: View {
|
||||
let week: [Date.WeekDay]
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 23) {
|
||||
ForEach(week) { day in
|
||||
VStack {
|
||||
Text(day.date.format("dd"))
|
||||
.font(.custom("Montserrat-SemiBold", fixedSize: 15))
|
||||
.foregroundStyle(getForegroundColor(day: day))
|
||||
}
|
||||
.frame(width: 30, height: 30, alignment: .center)
|
||||
.background(getBackgroundColor(day: day))
|
||||
.overlay(overlay(day: day))
|
||||
.cornerRadius(15)
|
||||
.onTapGesture {
|
||||
handleTap(day: day)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
Schedule-ICTIS/Main/Views/TabViews/WeekViewForWeek.swift
Normal file
@ -0,0 +1,76 @@
|
||||
//
|
||||
// WeekView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 20.12.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WeekViewForWeek: View {
|
||||
@Binding var weekSlider: [[Date.WeekDay]]
|
||||
@Binding var currentWeekIndex: Int
|
||||
@Binding var createWeek: Bool
|
||||
let week: [Date.WeekDay]
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
var body: some View {
|
||||
HStack (spacing: 10) {
|
||||
ForEach(week) { day in
|
||||
VStack (spacing: 1) {
|
||||
Text(day.date.format("E"))
|
||||
.font(.custom("Montserrat-SemiBold", fixedSize: 15))
|
||||
.foregroundColor(day.date.format("E") == "Вс" ? Color(.red) : isSameDate(day.date, vm.selectedDay) ? Color("customGray1") : Color("customGray3"))
|
||||
.padding(.top, 13)
|
||||
.foregroundColor(.gray)
|
||||
Text(day.date.format("dd"))
|
||||
.font(.custom("Montserrat-Semibold", fixedSize: 15))
|
||||
.foregroundStyle(isSameDate(day.date, vm.selectedDay) ? .white : .black)
|
||||
.padding(.bottom, 13)
|
||||
}
|
||||
.frame(width: 43, height: 55, alignment: .center)
|
||||
.background( content: {
|
||||
Group {
|
||||
if isSameDate(day.date, vm.selectedDay) {
|
||||
Color("blueColor")
|
||||
}
|
||||
else {
|
||||
Color(.white)
|
||||
}
|
||||
if isSameDate(day.date, vm.selectedDay) {
|
||||
Color("blueColor")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.overlay (
|
||||
Group {
|
||||
if day.date.isToday && !isSameDate(day.date, vm.selectedDay) {
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(Color("blueColor"), lineWidth: 2)
|
||||
}
|
||||
}
|
||||
)
|
||||
.cornerRadius(15)
|
||||
.onTapGesture {
|
||||
vm.selectedDay = day.date
|
||||
vm.updateSelectedDayIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
.background {
|
||||
GeometryReader {
|
||||
let minX = $0.frame(in: .global).minX
|
||||
|
||||
Color.clear
|
||||
.preference(key: OffsetKey.self, value: minX)
|
||||
.onPreferenceChange(OffsetKey.self) { value in
|
||||
if value.rounded() == 15 && createWeek {
|
||||
paginateWeek()
|
||||
|
||||
createWeek = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
Schedule-ICTIS/MockData.swift
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// MockData.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 06.12.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
struct MockData {
|
||||
static let daysOfWeek = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
|
||||
|
||||
// MARK: SheetCreateClassView
|
||||
static let notifications = ["Нет", "За 10 минут", "За 30 миннут", "За 1 час"]
|
||||
|
||||
static let onlineOrOffline = ["Оффлайн", "Онлайн"]
|
||||
|
||||
static let themes = ["Светлая", "Темная", "Системная"]
|
||||
|
||||
static let languages = ["Русский", "Английский", "Китайский", "Испанский"]
|
||||
|
||||
static let groups = ["КТбо2-6", "КТбо1-9", "КТбо3-3", "ВУЦ", "КТао1-1", "КТсо2-2"]
|
||||
}
|
91
Schedule-ICTIS/Model/CoreDataClassModel.swift
Normal file
@ -0,0 +1,91 @@
|
||||
//
|
||||
// Class.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 18.12.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
final class CoreDataClassModel: NSManagedObject, Identifiable {
|
||||
@NSManaged var auditory: String
|
||||
@NSManaged var professor: String
|
||||
@NSManaged var subject: String
|
||||
@NSManaged var comment: String
|
||||
@NSManaged var notification: String
|
||||
@NSManaged var day: Date
|
||||
@NSManaged var starttime: Date
|
||||
@NSManaged var endtime: Date
|
||||
@NSManaged var important: Bool
|
||||
@NSManaged var online: String
|
||||
|
||||
static var dateNow: Date = .now
|
||||
|
||||
// Здесь мы выполняем дополнительную инициализацию, назначая значения по умолчанию
|
||||
// Этот метод вызывается всякий раз, когда объект Core Data вставляется в контекст
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
|
||||
let moscowTimeZone = TimeZone(identifier: "Europe/Moscow")!
|
||||
var calendar = Calendar.current
|
||||
calendar.timeZone = moscowTimeZone
|
||||
let startTime = Date()
|
||||
let endTime = calendar.date(byAdding: .hour, value: 1, to: Date.init())
|
||||
|
||||
setPrimitiveValue("", forKey: "auditory")
|
||||
setPrimitiveValue("", forKey: "professor")
|
||||
setPrimitiveValue("", forKey: "subject")
|
||||
setPrimitiveValue("", forKey: "comment")
|
||||
setPrimitiveValue("Нет", forKey: "notification")
|
||||
setPrimitiveValue(false, forKey: "important")
|
||||
setPrimitiveValue("Оффлайн", forKey: "online")
|
||||
setPrimitiveValue(startTime, forKey: "day")
|
||||
setPrimitiveValue(startTime, forKey: "starttime")
|
||||
setPrimitiveValue(endTime, forKey: "endtime")
|
||||
}
|
||||
}
|
||||
|
||||
// Расширение для загрузки данных из памяти
|
||||
extension CoreDataClassModel {
|
||||
// Получаем все данные из памяти
|
||||
private static var classesFetchRequest: NSFetchRequest<CoreDataClassModel> {
|
||||
NSFetchRequest(entityName: "CoreDataClassModel")
|
||||
}
|
||||
|
||||
// Получаем все данные и сортируем их по дню
|
||||
// Этот метод будет использоваться на View(ScheduleView), где отображаются пары
|
||||
static func all() -> NSFetchRequest<CoreDataClassModel> {
|
||||
let request: NSFetchRequest<CoreDataClassModel> = classesFetchRequest
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \CoreDataClassModel.day, ascending: true)
|
||||
]
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
extension CoreDataClassModel {
|
||||
@discardableResult
|
||||
static func makePreview(count: Int, in context: NSManagedObjectContext) -> [CoreDataClassModel] {
|
||||
var classes = [CoreDataClassModel]()
|
||||
for i in 0..<count {
|
||||
let _class = CoreDataClassModel(context: context)
|
||||
_class.subject = "Предмет \(i)"
|
||||
_class.auditory = "Аудитория \(i)"
|
||||
_class.professor = "Преподаватель \(i)"
|
||||
_class.day = Calendar.current.date(byAdding: .day, value: i, to: .now) ?? .now
|
||||
_class.starttime = Date()
|
||||
_class.endtime = Calendar.current.date(byAdding: .hour, value: i, to: .now) ?? .now
|
||||
classes.append(_class)
|
||||
}
|
||||
return classes
|
||||
}
|
||||
|
||||
static func preview(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel {
|
||||
return makePreview(count: 1, in: context)[0]
|
||||
}
|
||||
|
||||
static func empty(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel {
|
||||
return CoreDataClassModel(context: context)
|
||||
}
|
||||
}
|
39
Schedule-ICTIS/Model/FavouriteGroupModel.swift
Normal file
@ -0,0 +1,39 @@
|
||||
//
|
||||
// FavouriteGroupsModel.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 04.04.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
final class FavouriteGroupModel: NSManagedObject, Identifiable {
|
||||
@NSManaged var name: String
|
||||
|
||||
// Здесь мы выполняем дополнительную инициализацию, назначая значения по умолчанию
|
||||
// Этот метод вызывается всякий раз, когда объект Core Data вставляется в контекст
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
|
||||
setPrimitiveValue("", forKey: "name")
|
||||
}
|
||||
}
|
||||
|
||||
// Расширение для загрузки данных из памяти
|
||||
extension FavouriteGroupModel {
|
||||
// Получаем все данные из памяти
|
||||
private static var favGroupsFetchRequest: NSFetchRequest<FavouriteGroupModel> {
|
||||
NSFetchRequest(entityName: "FavouriteGroupModel")
|
||||
}
|
||||
|
||||
// Получаем все данные и сортируем их по дню
|
||||
// Этот метод будет использоваться на View(ScheduleView), где отображаются пары
|
||||
static func all() -> NSFetchRequest<FavouriteGroupModel> {
|
||||
let request: NSFetchRequest<FavouriteGroupModel> = favGroupsFetchRequest
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \FavouriteGroupModel.name, ascending: true)
|
||||
]
|
||||
return request
|
||||
}
|
||||
}
|
39
Schedule-ICTIS/Model/FavouriteVpkModel.swift
Normal file
@ -0,0 +1,39 @@
|
||||
//
|
||||
// FavouriteGroupsModel.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 04.04.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
final class FavouriteVpkModel: NSManagedObject, Identifiable {
|
||||
@NSManaged var name: String
|
||||
|
||||
// Здесь мы выполняем дополнительную инициализацию, назначая значения по умолчанию
|
||||
// Этот метод вызывается всякий раз, когда объект Core Data вставляется в контекст
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
|
||||
setPrimitiveValue("", forKey: "name")
|
||||
}
|
||||
}
|
||||
|
||||
// Расширение для загрузки данных из памяти
|
||||
extension FavouriteVpkModel {
|
||||
// Получаем все данные из памяти
|
||||
private static var favVpkFetchRequest: NSFetchRequest<FavouriteVpkModel> {
|
||||
NSFetchRequest(entityName: "FavouriteVpkModel")
|
||||
}
|
||||
|
||||
// Получаем все данные и сортируем их по дню
|
||||
// Этот метод будет использоваться на View(ScheduleView), где отображаются пары
|
||||
static func all() -> NSFetchRequest<FavouriteVpkModel> {
|
||||
let request: NSFetchRequest<FavouriteVpkModel> = favVpkFetchRequest
|
||||
request.sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \FavouriteVpkModel.name, ascending: true)
|
||||
]
|
||||
return request
|
||||
}
|
||||
}
|
20
Schedule-ICTIS/Model/GroupsModel.swift
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// SubjectsModel.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 19.02.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Welcome
|
||||
struct Welcome: Decodable {
|
||||
let choices: [Subject]
|
||||
}
|
||||
|
||||
// MARK: - Choice
|
||||
struct Subject: Decodable, Identifiable {
|
||||
let name: String
|
||||
let id: String
|
||||
let group: String
|
||||
}
|
65
Schedule-ICTIS/Model/JsonClassModel.swift
Normal file
@ -0,0 +1,65 @@
|
||||
//
|
||||
// JsonClassModel.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 27.03.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
final class JsonClassModel: NSManagedObject, Identifiable {
|
||||
@NSManaged var name: String
|
||||
@NSManaged var group: String
|
||||
@NSManaged var time: String
|
||||
@NSManaged var day: Int16
|
||||
@NSManaged var week: 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
|
||||
}
|
||||
|
||||
static func deleteClasses(withName name: String, in context: NSManagedObjectContext) throws {
|
||||
let fetchRequest: NSFetchRequest<JsonClassModel> = JsonClassModel.all()
|
||||
fetchRequest.predicate = NSPredicate(format: "group == %@", name)
|
||||
|
||||
let groupsToDelete = try context.fetch(fetchRequest)
|
||||
|
||||
print("Пары для удаления: \(groupsToDelete)")
|
||||
|
||||
for group in groupsToDelete {
|
||||
do {
|
||||
try ClassProvider.shared.delete(group, in: context)
|
||||
}
|
||||
catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
Schedule-ICTIS/Model/ScheduleModel.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// Model.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 13.11.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Welcome
|
||||
struct Schedule: Decodable {
|
||||
let table: Table
|
||||
let weeks: [Int]
|
||||
}
|
||||
|
||||
// MARK: - Table
|
||||
struct Table: Decodable {
|
||||
let type, name: String
|
||||
let week: Int
|
||||
let group: String
|
||||
let table: [[String]]
|
||||
let link: String
|
||||
}
|
||||
|
||||
struct ClassInfo: Identifiable {
|
||||
let id = UUID()
|
||||
let subject: String
|
||||
let group: String
|
||||
let time: String
|
||||
}
|
14
Schedule-ICTIS/Model/TabBarModel.swift
Normal file
@ -0,0 +1,14 @@
|
||||
//
|
||||
// Tab.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 13.11.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum TabBarModel: String, CaseIterable {
|
||||
case tasks = "books.vertical"
|
||||
case schedule = "house"
|
||||
case settings = "gear"
|
||||
}
|
125
Schedule-ICTIS/Model/Utilities/Extensions/Date+Extensions.swift
Normal file
@ -0,0 +1,125 @@
|
||||
//
|
||||
// Date+Extensions.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 14.11.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
extension Date {
|
||||
func format(_ format: String, locale: Locale = Locale(identifier: "ru_RU")) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = format
|
||||
formatter.locale = locale
|
||||
let formattedString = formatter.string(from: self)
|
||||
|
||||
if format == "EEEE" {
|
||||
return formattedString.prefix(1).capitalized + formattedString.dropFirst()
|
||||
}
|
||||
|
||||
return formattedString
|
||||
}
|
||||
|
||||
var isToday: Bool {
|
||||
return Calendar.current.isDateInToday(self)
|
||||
}
|
||||
|
||||
func fetchWeek(_ date: Date = .init()) -> [WeekDay] {
|
||||
let calendar = Calendar.current
|
||||
let startOfDate = calendar.startOfDay(for: date)
|
||||
|
||||
var week: [WeekDay] = []
|
||||
// Создаем дату начала и конца недели
|
||||
let weekForDate = calendar.dateInterval(of: .weekOfMonth, for: startOfDate)
|
||||
//print("Start: \(weekForDate?.start)")
|
||||
//print("End: \(weekForDate?.end)")
|
||||
|
||||
guard let startOfWeek = weekForDate?.start else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Создаем массив дней для недели
|
||||
(0..<7).forEach { index in
|
||||
if let weekDay = calendar.date(byAdding: .day, value: index, to: startOfWeek) {
|
||||
week.append(WeekDay(date: weekDay))
|
||||
}
|
||||
}
|
||||
|
||||
return week
|
||||
}
|
||||
|
||||
func fetchMonth(_ date: Date = .init()) -> [MonthWeek] {
|
||||
let calendar = Calendar.current
|
||||
let startOfDate = calendar.startOfDay(for: date)
|
||||
|
||||
let weekForDate = calendar.dateInterval(of: .weekOfMonth, for: startOfDate)
|
||||
|
||||
guard let startOfWeek = weekForDate?.start else {
|
||||
return []
|
||||
}
|
||||
|
||||
var month: [MonthWeek] = []
|
||||
|
||||
for weekIndex in 0..<5 {
|
||||
var week: [WeekDay] = []
|
||||
for dayIndex in 0..<7 {
|
||||
if let weekDay = calendar.date(byAdding: .day, value: (weekIndex * 7 + dayIndex), to: startOfWeek) {
|
||||
week.append(WeekDay(date: weekDay))
|
||||
}
|
||||
}
|
||||
month.append(MonthWeek(week: week))
|
||||
}
|
||||
|
||||
return month
|
||||
}
|
||||
|
||||
func createNextMonth() -> [MonthWeek] {
|
||||
let calendar = Calendar.current
|
||||
let startOfLastDate = calendar.startOfDay(for: self)
|
||||
guard let nextDate = calendar.date(byAdding: .day, value: 1, to: startOfLastDate) else {
|
||||
return []
|
||||
}
|
||||
return fetchMonth(nextDate)
|
||||
}
|
||||
|
||||
func createPreviousMonth() -> [MonthWeek] {
|
||||
let calendar = Calendar.current
|
||||
let startOfFirstDate = calendar.startOfDay(for: self)
|
||||
guard let previousDate = calendar.date(byAdding: .weekOfMonth, value: -5, to: startOfFirstDate) else {
|
||||
return []
|
||||
}
|
||||
print("Start of first date \(startOfFirstDate)")
|
||||
print("Previous date \(previousDate)")
|
||||
return fetchMonth(previousDate)
|
||||
}
|
||||
|
||||
func createNextWeek() -> [WeekDay] {
|
||||
let calendar = Calendar.current
|
||||
let startOfLastDate = calendar.startOfDay(for: self)
|
||||
guard let nextDate = calendar.date(byAdding: .day, value: 1, to: startOfLastDate) else {
|
||||
return []
|
||||
}
|
||||
return fetchWeek(nextDate)
|
||||
}
|
||||
|
||||
func createPrevioustWeek() -> [WeekDay] {
|
||||
let calendar = Calendar.current
|
||||
let startOfFirstDate = calendar.startOfDay(for: self)
|
||||
guard let previousDate = calendar.date(byAdding: .day, value: -1, to: startOfFirstDate) else {
|
||||
return []
|
||||
}
|
||||
return fetchWeek(previousDate)
|
||||
}
|
||||
|
||||
struct WeekDay: Identifiable {
|
||||
var id: UUID = .init()
|
||||
var date: Date
|
||||
}
|
||||
|
||||
struct MonthWeek: Identifiable {
|
||||
var id: UUID = .init()
|
||||
var week: [WeekDay]
|
||||
}
|
||||
}
|
15
Schedule-ICTIS/Model/Utilities/Extensions/OffsetKey.swift
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// OffsetKey.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 18.11.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct OffsetKey: PreferenceKey {
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
//
|
||||
// ScheduleView+Extensions.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 04.04.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
extension ScheduleView {
|
||||
// Удаляем пары добавленные пользователем, если сегодня понедельник
|
||||
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.fetchWeekSchedule()
|
||||
do {
|
||||
try deleteAllJsonClassModelsSync()
|
||||
} catch {
|
||||
print("Ошибка при удалении: \(error)")
|
||||
return
|
||||
}
|
||||
saveGroupsToMemory()
|
||||
|
||||
// Сохраняем текущую дату как дату последнего выполнения
|
||||
UserDefaults.standard.set(today, forKey: "LastSaving")
|
||||
}
|
||||
}
|
||||
}
|
350
Schedule-ICTIS/Model/Utilities/Extensions/View+Extensions.swift
Normal file
@ -0,0 +1,350 @@
|
||||
//
|
||||
// View+Extensions.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 15.11.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func isDateInCurrentMonth(_ date: Date) -> Bool {
|
||||
let calendar = Calendar.current
|
||||
let currentDate = Date()
|
||||
|
||||
let currentMonth = calendar.component(.month, from: currentDate)
|
||||
let currentYear = calendar.component(.year, from: currentDate)
|
||||
|
||||
let dateMonth = calendar.component(.month, from: date)
|
||||
let dateYear = calendar.component(.year, from: date)
|
||||
|
||||
return currentMonth == dateMonth && currentYear == dateYear
|
||||
}
|
||||
|
||||
func isSameWeek(_ date1: Date, _ date2: Date) -> Bool {
|
||||
return Calendar.current.compare(date1, to: date2, toGranularity: .weekOfYear) == .orderedSame
|
||||
}
|
||||
|
||||
func weeksBetween(startDate: Date, endDate: Date) -> Int {
|
||||
let calendar = Calendar.current
|
||||
let startOfFirstDate = calendar.startOfDay(for: startDate)
|
||||
let startOfEndDate = calendar.startOfDay(for: endDate)
|
||||
|
||||
let weekForDate1 = calendar.dateInterval(of: .weekOfMonth, for: startOfFirstDate)
|
||||
let weekForDate2 = calendar.dateInterval(of: .weekOfMonth, for: startOfEndDate)
|
||||
|
||||
guard let startOfWeek1 = weekForDate1?.start else {
|
||||
return 0
|
||||
}
|
||||
|
||||
guard let startOfWeek2 = weekForDate2?.start else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let components = calendar.dateComponents([.day], from: startOfWeek1, to: startOfWeek2)
|
||||
let daysDifference = components.day ?? 0
|
||||
return Int(ceil(Double(abs(daysDifference)) / 7.0))
|
||||
}
|
||||
|
||||
func convertTimeString(_ input: String) -> [String] {
|
||||
let parts = input.split(separator: "-")
|
||||
if let firstPart = parts.first, let lastPart = parts.last {
|
||||
return [String(firstPart), String(lastPart)]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func getColorForClass(_ str: String) -> Color {
|
||||
if (str.contains("LMS")) {
|
||||
return Color("blueForOnline")
|
||||
}
|
||||
else if (str.contains("ВПК")) {
|
||||
return Color("turquoise")
|
||||
}
|
||||
else {
|
||||
return Color("greenForOffline")
|
||||
}
|
||||
}
|
||||
|
||||
func hoursMinutesAreEqual(date1: Date, isEqualTo date2: Date) -> Bool {
|
||||
let calendar = Calendar.current
|
||||
|
||||
let components1 = calendar.dateComponents([.day, .hour, .minute], from: date1)
|
||||
let components2 = calendar.dateComponents([.day, .hour, .minute], from: date2)
|
||||
|
||||
return components1.day == components2.day && components1.hour == components2.hour && components1.minute == components2.minute
|
||||
}
|
||||
|
||||
// MARK: ScheduleView
|
||||
func daysAreEqual(_ date1: Date, _ date2: Date) -> Bool {
|
||||
let calendar = Calendar.current
|
||||
|
||||
let components1 = calendar.dateComponents([.year, .month, .day], from: date1)
|
||||
let components2 = calendar.dateComponents([.year, .month, .day], from: date2)
|
||||
|
||||
return components1.year == components2.year &&
|
||||
components1.month == components2.month &&
|
||||
components1.day == components2.day
|
||||
}
|
||||
|
||||
func onlineOrNot(_ str: String) -> Color {
|
||||
if (str == "Онлайн") {
|
||||
return Color("blueForOnline")
|
||||
}
|
||||
else {
|
||||
return Color("greenForOffline")
|
||||
}
|
||||
}
|
||||
|
||||
func getSubjectName(_ subject: String, _ professor: String, _ auditory: String) -> String {
|
||||
return "\(subject) \(professor) \(auditory)"
|
||||
}
|
||||
|
||||
func getTimeString(_ date: Date) -> String {
|
||||
let calendar = Calendar.current
|
||||
let components = calendar.dateComponents([.hour, .minute], from: date)
|
||||
|
||||
guard let hour = components.hour, let minute = components.minute else {
|
||||
return "Invalid time"
|
||||
}
|
||||
|
||||
return String(format: "%02d:%02d", hour, minute)
|
||||
}
|
||||
|
||||
var dateFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .none
|
||||
return formatter
|
||||
}
|
||||
|
||||
var timeFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
return formatter
|
||||
}
|
||||
|
||||
func checkStartTimeLessThenEndTime(_ startTime: Date, _ endTime: Date) -> Bool {
|
||||
let calendar = Calendar.current
|
||||
|
||||
let firstComponents = calendar.dateComponents([.hour, .minute], from: startTime)
|
||||
let secondComponents = calendar.dateComponents([.hour, .minute], from: endTime)
|
||||
|
||||
guard let startHours = firstComponents.hour, let startMinutes = firstComponents.minute else {
|
||||
return false
|
||||
}
|
||||
guard let endHours = secondComponents.hour, let endMinutes = secondComponents.minute else {
|
||||
return false
|
||||
}
|
||||
|
||||
print("\(startHours) - \(endHours)")
|
||||
print("\(startMinutes) - \(endMinutes)")
|
||||
if Int(startHours) > Int(endHours) {
|
||||
return false
|
||||
}
|
||||
else if startHours == endHours {
|
||||
if startMinutes < endMinutes {
|
||||
return true
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WeekTabView {
|
||||
func updateWeekScreenViewForNewGroup() {
|
||||
vm.updateSelectedDayIndex()
|
||||
if weekSlider.isEmpty {
|
||||
let currentWeek = Date().fetchWeek(vm.selectedDay)
|
||||
|
||||
if let firstDate = currentWeek.first?.date {
|
||||
weekSlider.append(firstDate.createPrevioustWeek())
|
||||
}
|
||||
|
||||
weekSlider.append(currentWeek)
|
||||
|
||||
if let lastDate = currentWeek.last?.date {
|
||||
weekSlider.append(lastDate.createNextWeek())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WeekViewForWeek {
|
||||
func paginateWeek() {
|
||||
let calendar = Calendar.current
|
||||
let groupsKeys = Array (vm.nameToHtml.keys)
|
||||
if weekSlider.indices.contains(currentWeekIndex) {
|
||||
if let firstDate = weekSlider[currentWeekIndex].first?.date,
|
||||
currentWeekIndex == 0 {
|
||||
vm.week -= 1
|
||||
if !groupsKeys.isEmpty {
|
||||
vm.fetchWeekSchedule(isOtherWeek: true)
|
||||
}
|
||||
if UserDefaults.standard.string(forKey: "vpk") != nil {
|
||||
//vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk"))
|
||||
}
|
||||
weekSlider.insert(firstDate.createPrevioustWeek(), at: 0)
|
||||
weekSlider.removeLast()
|
||||
currentWeekIndex = 1
|
||||
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -1, to: vm.selectedDay) ?? Date.init()
|
||||
vm.updateSelectedDayIndex()
|
||||
}
|
||||
|
||||
if let lastDate = weekSlider[currentWeekIndex].last?.date,
|
||||
currentWeekIndex == (weekSlider.count - 1) {
|
||||
vm.week += 1
|
||||
if !groupsKeys.isEmpty {
|
||||
vm.fetchWeekSchedule(isOtherWeek: true)
|
||||
}
|
||||
weekSlider.append(lastDate.createNextWeek())
|
||||
weekSlider.removeFirst()
|
||||
currentWeekIndex = weekSlider.count - 2
|
||||
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 1, to: vm.selectedDay) ?? Date.init()
|
||||
vm.updateSelectedDayIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WeekViewForMonth {
|
||||
func getForegroundColor(day: Date.WeekDay) -> Color {
|
||||
if isDateInCurrentMonth(day.date) {
|
||||
return isSameDate(day.date, vm.selectedDay) ? .white : .black
|
||||
} else {
|
||||
return isSameDate(day.date, vm.selectedDay) ? .white : Color("greyForDaysInMonthTabView")
|
||||
}
|
||||
}
|
||||
|
||||
func getBackgroundColor(day: Date.WeekDay) -> Color {
|
||||
return isSameDate(day.date, vm.selectedDay) ? Color("blueColor") : Color("background")
|
||||
}
|
||||
|
||||
func overlay(day: Date.WeekDay) -> some View {
|
||||
Group {
|
||||
if day.date.isToday && !isSameDate(day.date, vm.selectedDay) {
|
||||
RoundedRectangle(cornerRadius: 100)
|
||||
.stroke(Color("blueColor"), lineWidth: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleTap(day: Date.WeekDay) {
|
||||
if isSameWeek(day.date, vm.selectedDay) {
|
||||
print("На одной неделе")
|
||||
}
|
||||
else {
|
||||
let groupsKeys = Array(vm.nameToHtml.keys)
|
||||
var difBetweenWeeks = weeksBetween(startDate: vm.selectedDay, endDate: day.date)
|
||||
if day.date < vm.selectedDay {
|
||||
difBetweenWeeks = difBetweenWeeks * -1
|
||||
}
|
||||
print(difBetweenWeeks)
|
||||
vm.week += difBetweenWeeks
|
||||
if !groupsKeys.isEmpty {
|
||||
vm.fetchWeekSchedule(isOtherWeek: true)
|
||||
}
|
||||
if UserDefaults.standard.string(forKey: "vpk") != nil {
|
||||
//vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk"))
|
||||
}
|
||||
}
|
||||
vm.selectedDay = day.date
|
||||
vm.updateSelectedDayIndex()
|
||||
}
|
||||
}
|
||||
|
||||
extension MonthTabView {
|
||||
func updateMonthScreenViewForNewGroup() {
|
||||
vm.updateSelectedDayIndex()
|
||||
if monthSlider.isEmpty {
|
||||
let currentMonth = Date().fetchMonth(vm.selectedDay)
|
||||
|
||||
if let firstDate = currentMonth.first?.week[0].date {
|
||||
let temp = firstDate.createPreviousMonth()
|
||||
print("First date - \(firstDate)")
|
||||
print(temp)
|
||||
monthSlider.append(temp)
|
||||
}
|
||||
|
||||
monthSlider.append(currentMonth)
|
||||
|
||||
if let lastDate = currentMonth.last?.week[6].date {
|
||||
let temp = lastDate.createNextMonth()
|
||||
monthSlider.append(temp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func paginateMonth(_ indexOfWeek: Int = 0) {
|
||||
let calendar = Calendar.current
|
||||
let groupsKeys = Array (vm.nameToHtml.keys)
|
||||
if monthSlider.indices.contains(currentMonthIndex) {
|
||||
if let firstDate = monthSlider[currentMonthIndex].first?.week[0].date,
|
||||
currentMonthIndex == 0 {
|
||||
monthSlider.insert(firstDate.createPreviousMonth(), at: 0)
|
||||
monthSlider.removeLast()
|
||||
currentMonthIndex = 1
|
||||
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -5, to: vm.selectedDay) ?? Date.init()
|
||||
vm.updateSelectedDayIndex()
|
||||
vm.week -= 5
|
||||
if !groupsKeys.isEmpty {
|
||||
vm.fetchWeekSchedule(isOtherWeek: true)
|
||||
}
|
||||
}
|
||||
|
||||
if let lastDate = monthSlider[currentMonthIndex].last?.week[6].date,
|
||||
currentMonthIndex == (monthSlider.count - 1) {
|
||||
monthSlider.append(lastDate.createNextMonth())
|
||||
monthSlider.removeFirst()
|
||||
currentMonthIndex = monthSlider.count - 2
|
||||
vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 5, to: vm.selectedDay) ?? Date.init()
|
||||
vm.updateSelectedDayIndex()
|
||||
vm.week += 5
|
||||
if !groupsKeys.isEmpty {
|
||||
vm.fetchWeekSchedule(isOtherWeek: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
Schedule-ICTIS/Model/Utilities/Network/NetworkError.swift
Normal file
@ -0,0 +1,46 @@
|
||||
//
|
||||
// NetworkError.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 18.11.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum NetworkError: String, Error, LocalizedError {
|
||||
case invalidUrl
|
||||
case invalidResponse
|
||||
case invalidData
|
||||
case noError
|
||||
case timeout
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidUrl:
|
||||
"InvalidUrl"
|
||||
case .invalidResponse:
|
||||
"InvalidResponse"
|
||||
case .invalidData:
|
||||
"Проверьте номер группы"
|
||||
case .timeout:
|
||||
"Ошибка сети"
|
||||
case .noError:
|
||||
"Нет ошибки"
|
||||
}
|
||||
}
|
||||
|
||||
var failureReason: String {
|
||||
switch self {
|
||||
case .invalidUrl:
|
||||
"Похоже не удалось составить ссылку для api"
|
||||
case .invalidResponse:
|
||||
"Для этой недели расписания еще нет"
|
||||
case .invalidData:
|
||||
"Похоже такой группы не существует"
|
||||
case .timeout:
|
||||
"Проверьте соединение с интернетом"
|
||||
case .noError:
|
||||
"Ошибки нет"
|
||||
}
|
||||
}
|
||||
}
|
81
Schedule-ICTIS/Model/Utilities/Network/NetworkManager.swift
Normal file
@ -0,0 +1,81 @@
|
||||
//
|
||||
// NetworkManager.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 18.11.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class NetworkManager {
|
||||
//"https://webictis.sfedu.ru/schedule-api/?group=51.html&week=15"
|
||||
//MARK: Properties
|
||||
static let shared = NetworkManager()
|
||||
private let decoder = JSONDecoder()
|
||||
private let urlForGroup = "https://shedule.rdcenter.ru/schedule-api/?query="
|
||||
private let urlForWeek = "https://shedule.rdcenter.ru/schedule-api/?group="
|
||||
private let customSession: URLSession // Кастомная сессия для ограничения времени ответа от сервера
|
||||
|
||||
//MARK: Initializer
|
||||
private init() {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.timeoutIntervalForRequest = 4 // Таймаут запроса
|
||||
configuration.timeoutIntervalForResource = 6 // Таймаут ресурса
|
||||
self.customSession = URLSession(configuration: configuration)
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
}
|
||||
|
||||
//MARK: Methods
|
||||
func makeUrlForGroup(_ group: String) -> String {
|
||||
return urlForGroup + group
|
||||
}
|
||||
|
||||
func makeUrlForWeek(_ numOfWeek: Int, _ htmlNameOfGroup: String) -> String {
|
||||
return urlForWeek + htmlNameOfGroup + "&week=" + String(numOfWeek)
|
||||
}
|
||||
|
||||
func getSchedule(_ group: String) async throws -> Schedule {
|
||||
let newUrlForGroup = makeUrlForGroup(group)
|
||||
print(newUrlForGroup)
|
||||
guard let url = URL(string: newUrlForGroup) else { throw NetworkError.invalidUrl }
|
||||
let (data, response) = try await customSession.data(from: url)
|
||||
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse }
|
||||
|
||||
do {
|
||||
return try decoder.decode(Schedule.self, from: data)
|
||||
}
|
||||
catch {
|
||||
throw NetworkError.invalidData
|
||||
}
|
||||
}
|
||||
|
||||
func getScheduleForOtherWeek(_ numOfWeek: Int, _ htmlNameOfGroup: String) async throws -> Schedule {
|
||||
let newUrlForWeek = makeUrlForWeek(numOfWeek, htmlNameOfGroup)
|
||||
print(newUrlForWeek)
|
||||
guard let url = URL(string: newUrlForWeek) else { throw NetworkError.invalidUrl }
|
||||
let (data, response) = try await customSession.data(from: url)
|
||||
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse }
|
||||
|
||||
do {
|
||||
return try decoder.decode(Schedule.self, from: data)
|
||||
}
|
||||
catch {
|
||||
throw NetworkError.invalidData
|
||||
}
|
||||
}
|
||||
|
||||
func getGroups(group: String) async throws -> Welcome {
|
||||
let newUrlForGroups = makeUrlForGroup(group)
|
||||
print(newUrlForGroups)
|
||||
guard let url = URL(string: newUrlForGroups) else { throw NetworkError.invalidUrl }
|
||||
let (data, response) = try await customSession.data(from: url)
|
||||
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse }
|
||||
|
||||
do {
|
||||
return try decoder.decode(Welcome.self, from: data)
|
||||
}
|
||||
catch {
|
||||
throw NetworkError.invalidData
|
||||
}
|
||||
}
|
||||
}
|
37
Schedule-ICTIS/Model/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()
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ICTIS_logo.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 24 KiB |
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
21
Schedule-ICTIS/Preview Content/Assets.xcassets/arrowRight.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "arrowRight.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
3
Schedule-ICTIS/Preview Content/Assets.xcassets/arrowRight.imageset/arrowRight.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 6L15 12L9 18" stroke="#8B8B8B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 212 B |
21
Schedule-ICTIS/Preview Content/Assets.xcassets/arrowdown.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "arrowdown.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
3
Schedule-ICTIS/Preview Content/Assets.xcassets/arrowdown.imageset/arrowdown.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 1L7 7L1 1" stroke="#007AFF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 191 B |
21
Schedule-ICTIS/Preview Content/Assets.xcassets/arrowup.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "arrowup.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
3
Schedule-ICTIS/Preview Content/Assets.xcassets/arrowup.imageset/arrowup.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 7L7 1L13 7" stroke="#007AFF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 191 B |
21
Schedule-ICTIS/Preview Content/Assets.xcassets/auditoryImage.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "auditoryImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Schedule-ICTIS/Preview Content/Assets.xcassets/auditoryImage.imageset/auditoryImage.png
vendored
Normal file
After Width: | Height: | Size: 549 B |
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xF6",
|
||||
"green" : "0xF0",
|
||||
"red" : "0xF1"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xA4",
|
||||
"green" : "0x60",
|
||||
"red" : "0x28"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xA4",
|
||||
"green" : "0x60",
|
||||
"red" : "0x28"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0x7A",
|
||||
"red" : "0x00"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
21
Schedule-ICTIS/Preview Content/Assets.xcassets/bookImage.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bookImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Schedule-ICTIS/Preview Content/Assets.xcassets/bookImage.imageset/bookImage.png
vendored
Normal file
After Width: | Height: | Size: 466 B |
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xD9",
|
||||
"green" : "0xD9",
|
||||
"red" : "0xD9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xD9",
|
||||
"green" : "0xD9",
|
||||
"red" : "0xD9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x7D",
|
||||
"green" : "0x7D",
|
||||
"red" : "0x7D"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x7D",
|
||||
"green" : "0x7D",
|
||||
"red" : "0x7D"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x7D",
|
||||
"green" : "0x7D",
|
||||
"red" : "0x7D"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x7D",
|
||||
"green" : "0x7D",
|
||||
"red" : "0x7D"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x7A",
|
||||
"green" : "0x7A",
|
||||
"red" : "0x7A"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x7A",
|
||||
"green" : "0x7A",
|
||||
"red" : "0x7A"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x84",
|
||||
"green" : "0x80",
|
||||
"red" : "0x80"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x84",
|
||||
"green" : "0x80",
|
||||
"red" : "0x80"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x0A",
|
||||
"green" : "0x97",
|
||||
"red" : "0x00"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x99",
|
||||
"green" : "0x99",
|
||||
"red" : "0x99"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x99",
|
||||
"green" : "0x99",
|
||||
"red" : "0x99"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
21
Schedule-ICTIS/Preview Content/Assets.xcassets/professorHatImage.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "professorHatImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Schedule-ICTIS/Preview Content/Assets.xcassets/professorHatImage.imageset/professorHatImage.png
vendored
Normal file
After Width: | Height: | Size: 716 B |
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xDE",
|
||||
"green" : "0xE4",
|
||||
"red" : "0x22"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xDE",
|
||||
"green" : "0xE4",
|
||||
"red" : "0x22"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
21
Schedule-ICTIS/Preview Content/Assets.xcassets/upDownArrows.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "upDownArrows.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
4
Schedule-ICTIS/Preview Content/Assets.xcassets/upDownArrows.imageset/upDownArrows.svg
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="14" height="18" viewBox="0 0 14 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 7L7 1L13 7" stroke="#878787" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13 11L7 17L1 11" stroke="#878787" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 320 B |
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
129
Schedule-ICTIS/Provider/ClassProvider.swift
Normal file
@ -0,0 +1,129 @@
|
||||
//
|
||||
// ClassProvider.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 18.12.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import SwiftUI
|
||||
|
||||
// Это класс служит посредником между View и моделью данных
|
||||
// Он позволяет открыть наш файл данных чтобы записывать и извлекать значения
|
||||
// Объект этого класса должен быть единственным за весь жизненный цикл приложения, чтобы не было рассинхронизации
|
||||
// Для этого мы делаем его синглтоном
|
||||
final class ClassProvider {
|
||||
static let shared = ClassProvider()
|
||||
|
||||
// Это свойство для хранения открытого файла модели данных
|
||||
private let persistentContainer: NSPersistentContainer
|
||||
|
||||
var viewContext: NSManagedObjectContext {
|
||||
persistentContainer.viewContext
|
||||
}
|
||||
|
||||
var newContext: NSManagedObjectContext {
|
||||
//persistentContainer.newBackgroundContext()
|
||||
//Можно использовать объявление newContext с помощью строки, которая написана выше, но вариант ниже потокобезопаснее
|
||||
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
|
||||
context.persistentStoreCoordinator = persistentContainer.persistentStoreCoordinator
|
||||
return context
|
||||
}
|
||||
|
||||
private init() {
|
||||
// Открытие файла
|
||||
persistentContainer = NSPersistentContainer(name: "ClassDataModel")
|
||||
if EnvironmentValues.isPreview {
|
||||
persistentContainer.persistentStoreDescriptions.first?.url = .init(filePath: "/dev/null")
|
||||
}
|
||||
|
||||
// Выставляем флаг для автоматического слияния данных из фонового контекста в основной
|
||||
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
|
||||
|
||||
// Выполняем открытие файла с данными
|
||||
persistentContainer.loadPersistentStores {_, error in
|
||||
if let error {
|
||||
fatalError("Unable to load store. Error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func exists(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) -> CoreDataClassModel? {
|
||||
try? context.existingObject(with: lesson.objectID) as? CoreDataClassModel
|
||||
}
|
||||
|
||||
func delete(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) throws {
|
||||
if let existingClass = exists(lesson, in: context) {
|
||||
context.delete(existingClass)
|
||||
Task(priority: .background) {
|
||||
try await context.perform {
|
||||
try context.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func persist(in context: NSManagedObjectContext) throws {
|
||||
if context.hasChanges {
|
||||
try context.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
static var isPreview: Bool {
|
||||
return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||
}
|
||||
}
|
||||
|
||||
extension ClassProvider {
|
||||
func exists(_ jsonClass: JsonClassModel, in context: NSManagedObjectContext) -> JsonClassModel? {
|
||||
try? context.existingObject(with: jsonClass.objectID) as? JsonClassModel
|
||||
}
|
||||
|
||||
func delete(_ jsonClass: JsonClassModel, in context: NSManagedObjectContext) throws {
|
||||
if let existingJsonClass = exists(jsonClass, in: context) {
|
||||
context.delete(existingJsonClass)
|
||||
Task(priority: .background) {
|
||||
try await context.perform {
|
||||
try context.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func exists(_ favGroup: FavouriteGroupModel, in context: NSManagedObjectContext) -> FavouriteGroupModel? {
|
||||
try? context.existingObject(with: favGroup.objectID) as? FavouriteGroupModel
|
||||
}
|
||||
|
||||
func delete(_ favGroup: FavouriteGroupModel, in context: NSManagedObjectContext) throws {
|
||||
if let existingFavGroup = exists(favGroup, in: context) {
|
||||
context.delete(existingFavGroup)
|
||||
Task(priority: .background) {
|
||||
try await context.perform {
|
||||
try context.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func exists(_ favVpk: FavouriteVpkModel, in context: NSManagedObjectContext) -> FavouriteVpkModel? {
|
||||
try? context.existingObject(with: favVpk.objectID) as? FavouriteVpkModel
|
||||
}
|
||||
|
||||
func delete(_ favVpk: FavouriteVpkModel, in context: NSManagedObjectContext) throws {
|
||||
if let existingFavVpk = exists(favVpk, in: context) {
|
||||
context.delete(existingFavVpk)
|
||||
Task(priority: .background) {
|
||||
try await context.perform {
|
||||
try context.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ClassProvider {
|
||||
|
||||
}
|
47
Schedule-ICTIS/Schedule_ICTISApp.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// Schedule_ICTISApp.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 13.11.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct Schedule_ICTISApp: App {
|
||||
@StateObject private var networkMonitor = NetworkMonitor()
|
||||
@StateObject var vm = ScheduleViewModel()
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(vm: vm, networkMonitor: networkMonitor)
|
||||
.environment(\.managedObjectContext, ClassProvider.shared.viewContext)
|
||||
.onAppear {
|
||||
fillDictForVm()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Schedule_ICTISApp {
|
||||
func fillDictForVm() {
|
||||
let context = ClassProvider.shared.viewContext
|
||||
|
||||
do {
|
||||
// Используем ваш метод all() для групп
|
||||
let groupRequest = FavouriteGroupModel.all()
|
||||
let groups = try context.fetch(groupRequest)
|
||||
for group in groups {
|
||||
vm.nameToHtml[group.name] = ""
|
||||
}
|
||||
|
||||
// Аналогично для ВПК (предполагая, что у вас есть аналогичный метод)
|
||||
let vpkRequest = FavouriteVpkModel.all()
|
||||
let vpks = try context.fetch(vpkRequest)
|
||||
for vpk in vpks {
|
||||
vm.nameToHtml[vpk.name] = ""
|
||||
}
|
||||
} catch {
|
||||
print("Ошибка при загрузке данных: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
71
Schedule-ICTIS/Settings/FavGroupsView.swift
Normal file
@ -0,0 +1,71 @@
|
||||
//
|
||||
// FavGroupsView.swift
|
||||
// Schedule ICTIS
|
||||
//
|
||||
// Created by Mironov Egor on 05.03.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct FavGroupsView: View {
|
||||
@ObservedObject var vm: ScheduleViewModel
|
||||
@ObservedObject var networkMonitor: NetworkMonitor
|
||||
@FetchRequest(fetchRequest: FavouriteGroupModel.all()) private var favGroups // Список групп сохраненных в CoreData
|
||||
var provider = ClassProvider.shared
|
||||
var body: some View {
|
||||
VStack (spacing: 0) {
|
||||
List {
|
||||
ForEach(favGroups, id: \.self) {favGroup in
|
||||
HStack {
|
||||
Text(favGroup.name)
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
Spacer()
|
||||
}
|
||||
.background(Color.white)
|
||||
.cornerRadius(10)
|
||||
.swipeActions(edge: .trailing) {
|
||||
Button(role: .destructive) {
|
||||
vm.removeFromSchedule(group: favGroup.name)
|
||||
do {
|
||||
try JsonClassModel.deleteClasses(withName: favGroup.name, in: provider.viewContext)
|
||||
try provider.delete(favGroup, in: provider.viewContext)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
} label: {
|
||||
Label("Удалить", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
if favGroups.count < 10 {
|
||||
NavigationLink(destination: SelectingGroupView(vm: vm, networkMonitor: networkMonitor)) {
|
||||
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)
|
||||
}
|
70
Schedule-ICTIS/Settings/FavVPKView.swift
Normal file
@ -0,0 +1,70 @@
|
||||
//
|
||||
// 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
|
||||
@FetchRequest(fetchRequest: FavouriteVpkModel.all()) private var favVpk // Список ВПК сохраненных в CoreData
|
||||
var provider = ClassProvider.shared
|
||||
var body: some View {
|
||||
VStack (spacing: 0) {
|
||||
List {
|
||||
ForEach(favVpk, id: \.self) {favVpk in
|
||||
HStack {
|
||||
Text(favVpk.name)
|
||||
.font(.custom("Montserrat-Medium", fixedSize: 17))
|
||||
Spacer()
|
||||
}
|
||||
.background(Color.white)
|
||||
.cornerRadius(10)
|
||||
.swipeActions(edge: .trailing) {
|
||||
Button(role: .destructive) {
|
||||
vm.removeFromSchedule(group: favVpk.name)
|
||||
do {
|
||||
try JsonClassModel.deleteClasses(withName: favVpk.name, in: provider.viewContext)
|
||||
try provider.delete(favVpk, in: provider.viewContext)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
} label: {
|
||||
Label("Удалить", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
if favVpk.count < 5 {
|
||||
NavigationLink(destination: SelectingVPKView(vm: vm, networkMonitor: networkMonitor)) {
|
||||
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)
|
||||
}
|