diff --git a/DHBW-Service.xcodeproj/project.pbxproj b/DHBW-Service.xcodeproj/project.pbxproj index 01dc31a..dda4ae1 100644 --- a/DHBW-Service.xcodeproj/project.pbxproj +++ b/DHBW-Service.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ CD2FC0C725A869FE00963178 /* alpaca-alt-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = CD2FC0C325A869FE00963178 /* alpaca-alt-icon@2x.png */; }; CD2FC0C825A869FE00963178 /* alpaca-alt-icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = CD2FC0C425A869FE00963178 /* alpaca-alt-icon@3x.png */; }; CD730A35259A860E00E0BB69 /* SettingsAcknowledgements.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD730A34259A860E00E0BB69 /* SettingsAcknowledgements.swift */; }; + CD8555BE25C47AE500C4ACD6 /* RaPlaFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8555BD25C47AE500C4ACD6 /* RaPlaFetcher.swift */; }; + CD8555C325C47B5300C4ACD6 /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8555C225C47B5300C4ACD6 /* ApiService.swift */; }; CD9FAB81258EC60200D6D0C5 /* DHBW_ServiceApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9FAB80258EC60200D6D0C5 /* DHBW_ServiceApp.swift */; }; CD9FAB83258EC60200D6D0C5 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9FAB82258EC60200D6D0C5 /* ContentView.swift */; }; CD9FAB85258EC60600D6D0C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD9FAB84258EC60600D6D0C5 /* Assets.xcassets */; }; @@ -28,6 +30,7 @@ CDDCF4842592028A0027CDC5 /* Localizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDCF4832592028A0027CDC5 /* Localizer.swift */; }; CDDCF493259203390027CDC5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CDDCF495259203390027CDC5 /* Localizable.strings */; }; CDDCF4A2259203B40027CDC5 /* General.strings in Resources */ = {isa = PBXBuildFile; fileRef = CDDCF4A4259203B40027CDC5 /* General.strings */; }; + CDEA70B225C6054F001CFE28 /* LecturePlanList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEA70B125C6054F001CFE28 /* LecturePlanList.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -53,6 +56,8 @@ CD2FC0C325A869FE00963178 /* alpaca-alt-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "alpaca-alt-icon@2x.png"; sourceTree = ""; }; CD2FC0C425A869FE00963178 /* alpaca-alt-icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "alpaca-alt-icon@3x.png"; sourceTree = ""; }; CD730A34259A860E00E0BB69 /* SettingsAcknowledgements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAcknowledgements.swift; sourceTree = ""; }; + CD8555BD25C47AE500C4ACD6 /* RaPlaFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RaPlaFetcher.swift; sourceTree = ""; }; + CD8555C225C47B5300C4ACD6 /* ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = ""; }; CD9FAB7D258EC60200D6D0C5 /* DHBW-Service.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DHBW-Service.app"; sourceTree = BUILT_PRODUCTS_DIR; }; CD9FAB80258EC60200D6D0C5 /* DHBW_ServiceApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DHBW_ServiceApp.swift; sourceTree = ""; }; CD9FAB82258EC60200D6D0C5 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -77,6 +82,7 @@ CDDCF4992592033F0027CDC5 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; CDDCF4A3259203B40027CDC5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/General.strings; sourceTree = ""; }; CDDCF4A8259203B80027CDC5 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/General.strings; sourceTree = ""; }; + CDEA70B125C6054F001CFE28 /* LecturePlanList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LecturePlanList.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -250,6 +256,7 @@ children = ( CDCD721925912E1200FBF2F5 /* HomeView.swift */, CDD39B4A259A64150078D05F /* SettingsMain.swift */, + CDEA70B125C6054F001CFE28 /* LecturePlanList.swift */, CD730A33259A85F500E0BB69 /* SettingsSubViews */, ); path = Tabs; @@ -275,6 +282,8 @@ isa = PBXGroup; children = ( CDDCF47A2591FE550027CDC5 /* UtilityFunctions.swift */, + CD8555BD25C47AE500C4ACD6 /* RaPlaFetcher.swift */, + CD8555C225C47B5300C4ACD6 /* ApiService.swift */, ); path = Utility; sourceTree = ""; @@ -431,9 +440,12 @@ CD730A35259A860E00E0BB69 /* SettingsAcknowledgements.swift in Sources */, CD9FAB83258EC60200D6D0C5 /* ContentView.swift in Sources */, CDCD72242591316500FBF2F5 /* LocalSettings.swift in Sources */, + CD8555BE25C47AE500C4ACD6 /* RaPlaFetcher.swift in Sources */, + CD8555C325C47B5300C4ACD6 /* ApiService.swift in Sources */, CD9FAB8D258EC60600D6D0C5 /* DHBW_Service.xcdatamodeld in Sources */, CDCD721A25912E1200FBF2F5 /* HomeView.swift in Sources */, CDD39B4B259A64150078D05F /* SettingsMain.swift in Sources */, + CDEA70B225C6054F001CFE28 /* LecturePlanList.swift in Sources */, CDDCF47B2591FE550027CDC5 /* UtilityFunctions.swift in Sources */, CD9FAB81258EC60200D6D0C5 /* DHBW_ServiceApp.swift in Sources */, ); diff --git a/DHBW-Service/CoreData/DHBW_Service.xcdatamodeld/DHBW_Service.xcdatamodel/contents b/DHBW-Service/CoreData/DHBW_Service.xcdatamodeld/DHBW_Service.xcdatamodel/contents index 43cf81e..64c2386 100644 --- a/DHBW-Service/CoreData/DHBW_Service.xcdatamodeld/DHBW_Service.xcdatamodel/contents +++ b/DHBW-Service/CoreData/DHBW_Service.xcdatamodeld/DHBW_Service.xcdatamodel/contents @@ -1,8 +1,16 @@ - + + + + + + + + + @@ -11,5 +19,6 @@ + \ No newline at end of file diff --git a/DHBW-Service/Utility/ApiService.swift b/DHBW-Service/Utility/ApiService.swift new file mode 100644 index 0000000..90f6092 --- /dev/null +++ b/DHBW-Service/Utility/ApiService.swift @@ -0,0 +1,89 @@ +// +// ApiService.swift +// DHBW-Service +// +// Created by Patrick Müller on 29.01.21. +// + +import Foundation + +class ApiService { + // MARK: HTTP POST Request + public class func callPost(url: URL, parameters: [String:Any], finish: @escaping ((message: String, data: Data?)) -> Void) { + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.addValue("application/json", forHTTPHeaderField: "Accept") + + // Set params + let postString = self.getPostString(params: parameters) + request.httpBody = postString.data(using: .utf8) + + var result: (message: String, data:Data?) = (message: "Fail", data: nil) + let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in + do { + guard error == nil else { + throw HttpFetchError.fetchError + } + + guard let data = data else { + throw HttpFetchError.noDataReceivedError + } + + result.message = "Success" + result.data = data + } catch let error { + print(error.localizedDescription) + } + + finish(result) + } + + task.resume() + } + + // MARK: HTTP GET Request + public class func callGet(url: URL, finish: @escaping ((message: String, data: Data?)) -> Void) { + var request = URLRequest(url: url) + request.httpMethod = "GET" + //request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + //request.addValue("application/json", forHTTPHeaderField: "Accept") + + var result: (message: String, data:Data?) = (message: "Fail", data: nil) + let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in + do { + guard error == nil else { + throw HttpFetchError.fetchError + } + + guard let data = data else { + throw HttpFetchError.noDataReceivedError + } + + result.message = "Success" + result.data = data + } catch let error { + print(error.localizedDescription) + } + + finish(result) + } + + task.resume() + } + + // MARK: Data preparation + private class func getPostString(params: [String:Any]) -> String { + var data = [String]() + for(key, value) in params{ + data.append(key + "=\(value)") + } + return data.map{String($0)}.joined(separator: "&") + } + + // MARK: Error enum + private enum HttpFetchError: Error { + case fetchError + case noDataReceivedError + } +} diff --git a/DHBW-Service/Utility/RaPlaFetcher.swift b/DHBW-Service/Utility/RaPlaFetcher.swift new file mode 100644 index 0000000..f85bbbb --- /dev/null +++ b/DHBW-Service/Utility/RaPlaFetcher.swift @@ -0,0 +1,138 @@ +// +// RaPlaFetcher.swift +// DHBW-Service +// +// Created by Patrick Müller on 29.01.21. +// + +import Foundation +import CoreData + +class RaPlaFetcher { + public class iCalEvent { + var startDate: Date = Date() //DTSTART + var endDate: Date = Date() //DTEND + var summary: String = "" //SUMMARY + var description: String = "" //DESCRIPTION + var location: String = "" //LOCATION + var category: String = "" //CATEGORIES + } + + // Get the RaPla file from the given URL and save the events to CoreData + public class func getRaplaFileAndSaveToCoreData(from urlString: String) -> Bool { + let file = getFileAsString(from: urlString) + + let eventStrings = splitIntoEvents(file: file) + + let eventObjects = convertStringsToObjects(eventStrings: eventStrings) + + return saveToCoreData(eventObjects: eventObjects) + } + + // Get the RaPla files from the given URL and return the event objects + public class func getRaplaFileAndReturnEvents(from urlString: String) -> [iCalEvent] { + let file = getFileAsString(from: urlString) + + let eventStrings = splitIntoEvents(file: file) + + return convertStringsToObjects(eventStrings: eventStrings) + } + + // GET the file from the given URL and convert it to a String that is then returned + private class func getFileAsString(from urlString: String) -> String { + let url = URL(string: urlString)! + var file: String = "" + + do { + file = try String(contentsOf: url, encoding: .utf8) + } catch let error { + print(error.localizedDescription) + } + + return file + } + + // Split the given ical file string into individual event strings and return them as a list + private class func splitIntoEvents(file: String) -> [String] { + let regexOptions: NSRegularExpression.Options = [.dotMatchesLineSeparators] + // Regex explanation: Matches BEGIN:VEVENT "Any character" END:VEVENT across multiple lines. The *? assures that we receive the + // maximum amount of matches, i.e. it makes the regex non-greedy as we would otherwise just receive one giant match + let eventStrings = UtilityFunctions.regexMatches(for: "BEGIN:VEVENT.*?END:VEVENT", with: regexOptions, in: file) + + return eventStrings + } + + // Convert an ical event String into an iCalEvent Codable object as defined above + private class func convertStringsToObjects(eventStrings: [String]) -> [iCalEvent] { + var events: [iCalEvent] = [] + + for eventString in eventStrings { + let lines = eventString.components(separatedBy: .newlines) + let evt = iCalEvent() + + for line in lines { + var lineWithoutPrefix = line + if(!line.contains(":")) { + continue + } + lineWithoutPrefix.removeSubrange(lineWithoutPrefix.startIndex...lineWithoutPrefix.firstIndex(of: ":")!) + + if(line.hasPrefix("DTSTART")){ + //Date format: 20181101T080000 + let dateFormatter = DateFormatter() + if(lineWithoutPrefix.contains("Z")){ + dateFormatter.dateFormat = "yyyyMMdd'T'HHmmssZ" + } else { + dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss" + } + let date = dateFormatter.date(from: lineWithoutPrefix)! + evt.startDate = date + } else if(line.hasPrefix("DTEND")){ + let dateFormatter = DateFormatter() + if(lineWithoutPrefix.contains("Z")){ + dateFormatter.dateFormat = "yyyyMMdd'T'HHmmssZ" + } else { + dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss" + } + let date = dateFormatter.date(from: lineWithoutPrefix)! + evt.endDate = date + } else if(line.hasPrefix("SUMMARY")){ + evt.summary = lineWithoutPrefix + } else if(line.hasPrefix("DESCRIPTION")){ + evt.description = lineWithoutPrefix + } else if(line.hasPrefix("LOCATION")){ + evt.location = lineWithoutPrefix + } else if(line.hasPrefix("CATEGORIES")){ + evt.category = lineWithoutPrefix + } + } + + events.append(evt) + } + + return events + } + + // Save the given iCalEvent objects to CoreData + private class func saveToCoreData(eventObjects: [iCalEvent]) -> Bool{ + // Delete old data + if(UtilityFunctions.deleteAllCoreDataEntitiesOfType(type: "RaPlaEvent")){ + for event in eventObjects { + let entity = NSEntityDescription.entity(forEntityName: "RaPlaEvent", in: PersistenceController.shared.context)! + let evt = NSManagedObject(entity: entity, insertInto: PersistenceController.shared.context) + evt.setValue(event.startDate, forKey: "startDate") + evt.setValue(event.endDate, forKey: "endDate") + evt.setValue(event.summary, forKey: "summary") + evt.setValue(event.description, forKey: "descr") + evt.setValue(event.location, forKey: "location") + evt.setValue(event.category, forKey: "category") + } + + PersistenceController.shared.save() + + return true + } else { + return false + } + } +} diff --git a/DHBW-Service/Utility/UtilityFunctions.swift b/DHBW-Service/Utility/UtilityFunctions.swift index 330e1cf..fc63fe6 100644 --- a/DHBW-Service/Utility/UtilityFunctions.swift +++ b/DHBW-Service/Utility/UtilityFunctions.swift @@ -9,12 +9,13 @@ import Foundation import CoreData class UtilityFunctions { - public class func getCoreDataObject(entity: String) -> [NSManagedObject]{ + public class func getCoreDataObject(entity: String, sortDescriptors: [NSSortDescriptor]) -> [NSManagedObject]{ let managedContext = PersistenceController.shared.context let fetchRequest = NSFetchRequest(entityName: entity) + fetchRequest.sortDescriptors = sortDescriptors do { return try managedContext.fetch(fetchRequest) @@ -60,4 +61,20 @@ class UtilityFunctions { return allSuccessful } + + // MARK: Find matches in the given text for the given regex string. + public class func regexMatches(for regex: String, with options: NSRegularExpression.Options, in text: String) -> [String] { + + do { + let regex = try NSRegularExpression(pattern: regex, options: options) + let results = regex.matches(in: text, + range: NSRange(text.startIndex..., in: text)) + return results.map { + String(text[Range($0.range, in: text)!]) + } + } catch let error { + print("invalid regex: \(error.localizedDescription)") + return [] + } + } } diff --git a/DHBW-Service/Views/ContentView.swift b/DHBW-Service/Views/ContentView.swift index 223c9e6..130b93a 100644 --- a/DHBW-Service/Views/ContentView.swift +++ b/DHBW-Service/Views/ContentView.swift @@ -26,6 +26,14 @@ struct ContentView: View { } } .tag(0) + LecturePlanList() + .tabItem { + VStack { + Image(systemName: "calendar") + Text("Lecture Plan") + } + } + .tag(1) SettingsMain() .tabItem { VStack { @@ -33,10 +41,14 @@ struct ContentView: View { Text("Settings") } } - .tag(1) + .tag(2) } } } + .onAppear{ + // Called upon the opening of the app + RaPlaFetcher.getRaplaFileAndSaveToCoreData(from: "https://rapla.dhbw-karlsruhe.de/rapla?page=ical&user=eisenbiegler&file=TINF19B4") + } } } diff --git a/DHBW-Service/Views/Tabs/HomeView.swift b/DHBW-Service/Views/Tabs/HomeView.swift index 8bf2efa..a9af59c 100644 --- a/DHBW-Service/Views/Tabs/HomeView.swift +++ b/DHBW-Service/Views/Tabs/HomeView.swift @@ -36,7 +36,7 @@ struct HomeView: View { extension HomeView{ func readFromCoreData() { - let fetchedData = UtilityFunctions.getCoreDataObject(entity: "User") + let fetchedData = UtilityFunctions.getCoreDataObject(entity: "User", sortDescriptors: []) if(!fetchedData.isEmpty) { let user = fetchedData[0] diff --git a/DHBW-Service/Views/Tabs/LecturePlanList.swift b/DHBW-Service/Views/Tabs/LecturePlanList.swift new file mode 100644 index 0000000..27e100d --- /dev/null +++ b/DHBW-Service/Views/Tabs/LecturePlanList.swift @@ -0,0 +1,42 @@ +// +// LecturePlanList.swift +// DHBW-Service +// +// Created by Patrick Müller on 30.01.21. +// + +import SwiftUI +import CoreData + +struct LecturePlanList: View { + @State private var events: [NSManagedObject] = [] + + var body: some View { + List { + ForEach(events, id: \.self) { event in + HStack { + Text(formatDate(date: event.value(forKeyPath: "startDate") as! Date)) + Text(event.value(forKeyPath: "summary") as! String) + } + } + }.onAppear{ + let sectionSortDescriptor = NSSortDescriptor(key: "startDate", ascending: true) + let sortDescriptors = [sectionSortDescriptor] + self.events = UtilityFunctions.getCoreDataObject(entity: "RaPlaEvent", sortDescriptors: sortDescriptors) + } + } +} + +struct LecturePlanList_Previews: PreviewProvider { + static var previews: some View { + LecturePlanList() + } +} + +extension LecturePlanList { + private func formatDate(date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .short + return formatter.string(from: date) + } +}