diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..adca61c Binary files /dev/null and b/.DS_Store differ diff --git a/Schedule_B.xcodeproj/project.pbxproj b/Schedule_B.xcodeproj/project.pbxproj index 256a5de..1c3e5d6 100644 --- a/Schedule_B.xcodeproj/project.pbxproj +++ b/Schedule_B.xcodeproj/project.pbxproj @@ -7,6 +7,17 @@ objects = { /* Begin PBXBuildFile section */ + 1C3A75F725E4D2E300415F32 /* CalendarExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A75F625E4D2E300415F32 /* CalendarExtension.swift */; }; + 1C3A75FA25E4D3A600415F32 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A75F925E4D3A600415F32 /* ColorExtension.swift */; }; + 1C3A75FD25E4D57700415F32 /* ScrollViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A75FC25E4D57700415F32 /* ScrollViewExtension.swift */; }; + 1C3A760025E4D5AE00415F32 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A75FF25E4D5AE00415F32 /* UIViewExtension.swift */; }; + 1C3A760A25E4D69E00415F32 /* CellContentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A760925E4D69E00415F32 /* CellContentsView.swift */; }; + 1C3A760D25E4D6CE00415F32 /* CalenderCellVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A760C25E4D6CE00415F32 /* CalenderCellVC.swift */; }; + 1C3A761025E4D6FB00415F32 /* ScheduleModelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A760F25E4D6FB00415F32 /* ScheduleModelController.swift */; }; + 1C3A761325E4D72400415F32 /* Schedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A761225E4D72400415F32 /* Schedule.swift */; }; + 1C3A761625E4D74900415F32 /* HolidayGather.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A761525E4D74900415F32 /* HolidayGather.swift */; }; + 1C3A761925E4D77100415F32 /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A761825E4D77100415F32 /* NotificationController.swift */; }; + 1C3A761C25E4D81C00415F32 /* SingleCalendarVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A761B25E4D81C00415F32 /* SingleCalendarVC.swift */; }; 1C3E985425DAABFE00268DB0 /* DetailMemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3E985325DAABFE00268DB0 /* DetailMemoViewController.swift */; }; 1C9948B125D1128600ACDBB4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9948B025D1128600ACDBB4 /* AppDelegate.swift */; }; 1C9948B325D1128600ACDBB4 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9948B225D1128600ACDBB4 /* SceneDelegate.swift */; }; @@ -17,12 +28,42 @@ 1C9948C725D1136000ACDBB4 /* FSCalendar in Frameworks */ = {isa = PBXBuildFile; productRef = 1C9948C625D1136000ACDBB4 /* FSCalendar */; }; 1CA2BC8425D249FB0044EA31 /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CA2BC8325D249FB0044EA31 /* MenuViewController.swift */; }; 1CA2BC8825D24A6E0044EA31 /* MenuTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CA2BC8725D24A6E0044EA31 /* MenuTableViewCell.swift */; }; - 1CE8AF1525D25182004A8F37 /* ScheduleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CE8AF1425D25182004A8F37 /* ScheduleViewController.swift */; }; + 1CE8AF1525D25182004A8F37 /* ScheduleCollectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CE8AF1425D25182004A8F37 /* ScheduleCollectionVC.swift */; }; 84BDC8D325D64F5B00144860 /* MemoListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BDC8D225D64F5B00144860 /* MemoListTableViewCell.swift */; }; 84BDC8D625D64FD200144860 /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BDC8D525D64FD200144860 /* Memo.swift */; }; + CC99BC3125E4DBD200F2CCC7 /* ScheduleCalendarVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3025E4DBD200F2CCC7 /* ScheduleCalendarVC.swift */; }; + CC99BC4425E4DC1600F2CCC7 /* SettingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3425E4DC1500F2CCC7 /* SettingViewController.swift */; }; + CC99BC4525E4DC1600F2CCC7 /* ConnectGoogleCalendarVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3625E4DC1500F2CCC7 /* ConnectGoogleCalendarVC.swift */; }; + CC99BC4625E4DC1600F2CCC7 /* GoogleOAuthConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3725E4DC1500F2CCC7 /* GoogleOAuthConfig.swift */; }; + CC99BC4725E4DC1600F2CCC7 /* CalendarComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3925E4DC1600F2CCC7 /* CalendarComponent.swift */; }; + CC99BC4825E4DC1600F2CCC7 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3A25E4DC1600F2CCC7 /* DateExtension.swift */; }; + CC99BC4925E4DC1600F2CCC7 /* ICalendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3B25E4DC1600F2CCC7 /* ICalendar.swift */; }; + CC99BC4A25E4DC1600F2CCC7 /* ICalendarAlarm.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3C25E4DC1600F2CCC7 /* ICalendarAlarm.swift */; }; + CC99BC4B25E4DC1600F2CCC7 /* ICalendarError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3D25E4DC1600F2CCC7 /* ICalendarError.swift */; }; + CC99BC4C25E4DC1600F2CCC7 /* ICalendarEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3E25E4DC1600F2CCC7 /* ICalendarEvent.swift */; }; + CC99BC4D25E4DC1600F2CCC7 /* ICalendarParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC3F25E4DC1600F2CCC7 /* ICalendarParser.swift */; }; + CC99BC4E25E4DC1600F2CCC7 /* IcsElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC4025E4DC1600F2CCC7 /* IcsElement.swift */; }; + CC99BC4F25E4DC1600F2CCC7 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC4125E4DC1600F2CCC7 /* StringExtension.swift */; }; + CC99BC5025E4DC1600F2CCC7 /* OAuthGather.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC4225E4DC1600F2CCC7 /* OAuthGather.swift */; }; + CC99BC5125E4DC1600F2CCC7 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC4325E4DC1600F2CCC7 /* WebViewController.swift */; }; + CC99BC5825E4DCF200F2CCC7 /* ViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC5425E4DCF200F2CCC7 /* ViewControllerExtension.swift */; }; + CC99BC5925E4DCF200F2CCC7 /* Future&Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC5525E4DCF200F2CCC7 /* Future&Promise.swift */; }; + CC99BC5A25E4DCF200F2CCC7 /* URLRequestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC5625E4DCF200F2CCC7 /* URLRequestExtension.swift */; }; + CC99BC5B25E4DCF200F2CCC7 /* Loopable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC99BC5725E4DCF200F2CCC7 /* Loopable.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1C3A75F625E4D2E300415F32 /* CalendarExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarExtension.swift; sourceTree = ""; }; + 1C3A75F925E4D3A600415F32 /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; + 1C3A75FC25E4D57700415F32 /* ScrollViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewExtension.swift; sourceTree = ""; }; + 1C3A75FF25E4D5AE00415F32 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; + 1C3A760925E4D69E00415F32 /* CellContentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellContentsView.swift; sourceTree = ""; }; + 1C3A760C25E4D6CE00415F32 /* CalenderCellVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalenderCellVC.swift; sourceTree = ""; }; + 1C3A760F25E4D6FB00415F32 /* ScheduleModelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleModelController.swift; sourceTree = ""; }; + 1C3A761225E4D72400415F32 /* Schedule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Schedule.swift; sourceTree = ""; }; + 1C3A761525E4D74900415F32 /* HolidayGather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HolidayGather.swift; sourceTree = ""; }; + 1C3A761825E4D77100415F32 /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = ""; }; + 1C3A761B25E4D81C00415F32 /* SingleCalendarVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleCalendarVC.swift; sourceTree = ""; }; 1C3E985325DAABFE00268DB0 /* DetailMemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailMemoViewController.swift; sourceTree = ""; wrapsLines = 0; }; 1C9948AD25D1128600ACDBB4 /* Schedule_B.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Schedule_B.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1C9948B025D1128600ACDBB4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -34,9 +75,28 @@ 1C9948BE25D1128700ACDBB4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1CA2BC8325D249FB0044EA31 /* MenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuViewController.swift; sourceTree = ""; }; 1CA2BC8725D24A6E0044EA31 /* MenuTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuTableViewCell.swift; sourceTree = ""; }; - 1CE8AF1425D25182004A8F37 /* ScheduleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleViewController.swift; sourceTree = ""; }; + 1CE8AF1425D25182004A8F37 /* ScheduleCollectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleCollectionVC.swift; sourceTree = ""; }; 84BDC8D225D64F5B00144860 /* MemoListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoListTableViewCell.swift; sourceTree = ""; }; 84BDC8D525D64FD200144860 /* Memo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memo.swift; sourceTree = ""; }; + CC99BC3025E4DBD200F2CCC7 /* ScheduleCalendarVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduleCalendarVC.swift; sourceTree = ""; }; + CC99BC3425E4DC1500F2CCC7 /* SettingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingViewController.swift; sourceTree = ""; }; + CC99BC3625E4DC1500F2CCC7 /* ConnectGoogleCalendarVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectGoogleCalendarVC.swift; sourceTree = ""; }; + CC99BC3725E4DC1500F2CCC7 /* GoogleOAuthConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleOAuthConfig.swift; sourceTree = ""; }; + CC99BC3925E4DC1600F2CCC7 /* CalendarComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarComponent.swift; sourceTree = ""; }; + CC99BC3A25E4DC1600F2CCC7 /* DateExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; + CC99BC3B25E4DC1600F2CCC7 /* ICalendar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICalendar.swift; sourceTree = ""; }; + CC99BC3C25E4DC1600F2CCC7 /* ICalendarAlarm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICalendarAlarm.swift; sourceTree = ""; }; + CC99BC3D25E4DC1600F2CCC7 /* ICalendarError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICalendarError.swift; sourceTree = ""; }; + CC99BC3E25E4DC1600F2CCC7 /* ICalendarEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICalendarEvent.swift; sourceTree = ""; }; + CC99BC3F25E4DC1600F2CCC7 /* ICalendarParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICalendarParser.swift; sourceTree = ""; }; + CC99BC4025E4DC1600F2CCC7 /* IcsElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IcsElement.swift; sourceTree = ""; }; + CC99BC4125E4DC1600F2CCC7 /* StringExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; + CC99BC4225E4DC1600F2CCC7 /* OAuthGather.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuthGather.swift; sourceTree = ""; }; + CC99BC4325E4DC1600F2CCC7 /* WebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; + CC99BC5425E4DCF200F2CCC7 /* ViewControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerExtension.swift; sourceTree = ""; }; + CC99BC5525E4DCF200F2CCC7 /* Future&Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Future&Promise.swift"; sourceTree = ""; }; + CC99BC5625E4DCF200F2CCC7 /* URLRequestExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestExtension.swift; sourceTree = ""; }; + CC99BC5725E4DCF200F2CCC7 /* Loopable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Loopable.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -51,10 +111,83 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1C3A75F125E4CCB200415F32 /* Setting */ = { + isa = PBXGroup; + children = ( + CC99BC3525E4DC1500F2CCC7 /* GoogleCalendar */, + CC99BC3425E4DC1500F2CCC7 /* SettingViewController.swift */, + ); + name = Setting; + sourceTree = ""; + }; + 1C3A760225E4D5C900415F32 /* Helper */ = { + isa = PBXGroup; + children = ( + 1CE8AF1425D25182004A8F37 /* ScheduleCollectionVC.swift */, + 1C3A75F625E4D2E300415F32 /* CalendarExtension.swift */, + CC99BC5525E4DCF200F2CCC7 /* Future&Promise.swift */, + CC99BC5725E4DCF200F2CCC7 /* Loopable.swift */, + CC99BC5625E4DCF200F2CCC7 /* URLRequestExtension.swift */, + CC99BC5425E4DCF200F2CCC7 /* ViewControllerExtension.swift */, + 1C3A75F925E4D3A600415F32 /* ColorExtension.swift */, + 1C3A75FC25E4D57700415F32 /* ScrollViewExtension.swift */, + 1C3A75FF25E4D5AE00415F32 /* UIViewExtension.swift */, + ); + name = Helper; + sourceTree = ""; + }; + 1C3A760425E4D62A00415F32 /* View */ = { + isa = PBXGroup; + children = ( + 1C3A760525E4D63600415F32 /* ViewController */, + ); + name = View; + sourceTree = ""; + }; + 1C3A760525E4D63600415F32 /* ViewController */ = { + isa = PBXGroup; + children = ( + 1C3A760725E4D64900415F32 /* Calendar */, + 1C3A760625E4D64000415F32 /* Daily */, + ); + name = ViewController; + sourceTree = ""; + }; + 1C3A760625E4D64000415F32 /* Daily */ = { + isa = PBXGroup; + children = ( + ); + name = Daily; + sourceTree = ""; + }; + 1C3A760725E4D64900415F32 /* Calendar */ = { + isa = PBXGroup; + children = ( + 1C3A760C25E4D6CE00415F32 /* CalenderCellVC.swift */, + CC99BC3025E4DBD200F2CCC7 /* ScheduleCalendarVC.swift */, + 1C3A761B25E4D81C00415F32 /* SingleCalendarVC.swift */, + ); + name = Calendar; + sourceTree = ""; + }; + 1C3A760825E4D65700415F32 /* Model */ = { + isa = PBXGroup; + children = ( + 1C3A760F25E4D6FB00415F32 /* ScheduleModelController.swift */, + 1C3A761225E4D72400415F32 /* Schedule.swift */, + 1C3A761525E4D74900415F32 /* HolidayGather.swift */, + 1C3A761825E4D77100415F32 /* NotificationController.swift */, + ); + name = Model; + sourceTree = ""; + }; 1C3E984D25DAAB3E00268DB0 /* Schedule */ = { isa = PBXGroup; children = ( - 1CE8AF1425D25182004A8F37 /* ScheduleViewController.swift */, + 1C3A760825E4D65700415F32 /* Model */, + 1C3A760425E4D62A00415F32 /* View */, + 1C3A760225E4D5C900415F32 /* Helper */, + 1C3A760925E4D69E00415F32 /* CellContentsView.swift */, ); name = Schedule; sourceTree = ""; @@ -111,6 +244,7 @@ 1C9948AF25D1128600ACDBB4 /* Schedule_B */ = { isa = PBXGroup; children = ( + 1C3A75F125E4CCB200415F32 /* Setting */, 1C3E985025DAAB7E00268DB0 /* Etc */, 1C3E984F25DAAB6D00268DB0 /* Menu */, 1C3E984E25DAAB5000268DB0 /* Diary */, @@ -120,6 +254,34 @@ path = Schedule_B; sourceTree = ""; }; + CC99BC3525E4DC1500F2CCC7 /* GoogleCalendar */ = { + isa = PBXGroup; + children = ( + CC99BC3625E4DC1500F2CCC7 /* ConnectGoogleCalendarVC.swift */, + CC99BC3725E4DC1500F2CCC7 /* GoogleOAuthConfig.swift */, + CC99BC3825E4DC1600F2CCC7 /* ICalendarLibrary */, + CC99BC4225E4DC1600F2CCC7 /* OAuthGather.swift */, + CC99BC4325E4DC1600F2CCC7 /* WebViewController.swift */, + ); + path = GoogleCalendar; + sourceTree = ""; + }; + CC99BC3825E4DC1600F2CCC7 /* ICalendarLibrary */ = { + isa = PBXGroup; + children = ( + CC99BC3925E4DC1600F2CCC7 /* CalendarComponent.swift */, + CC99BC3A25E4DC1600F2CCC7 /* DateExtension.swift */, + CC99BC3B25E4DC1600F2CCC7 /* ICalendar.swift */, + CC99BC3C25E4DC1600F2CCC7 /* ICalendarAlarm.swift */, + CC99BC3D25E4DC1600F2CCC7 /* ICalendarError.swift */, + CC99BC3E25E4DC1600F2CCC7 /* ICalendarEvent.swift */, + CC99BC3F25E4DC1600F2CCC7 /* ICalendarParser.swift */, + CC99BC4025E4DC1600F2CCC7 /* IcsElement.swift */, + CC99BC4125E4DC1600F2CCC7 /* StringExtension.swift */, + ); + path = ICalendarLibrary; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -196,15 +358,45 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1C3A761625E4D74900415F32 /* HolidayGather.swift in Sources */, + CC99BC4425E4DC1600F2CCC7 /* SettingViewController.swift in Sources */, + CC99BC4B25E4DC1600F2CCC7 /* ICalendarError.swift in Sources */, 1C9948B525D1128600ACDBB4 /* DiaryViewController.swift in Sources */, 1C9948B125D1128600ACDBB4 /* AppDelegate.swift in Sources */, + CC99BC4725E4DC1600F2CCC7 /* CalendarComponent.swift in Sources */, + CC99BC4825E4DC1600F2CCC7 /* DateExtension.swift in Sources */, 1C9948B325D1128600ACDBB4 /* SceneDelegate.swift in Sources */, + CC99BC5925E4DCF200F2CCC7 /* Future&Promise.swift in Sources */, + 1C3A75FA25E4D3A600415F32 /* ColorExtension.swift in Sources */, + CC99BC4625E4DC1600F2CCC7 /* GoogleOAuthConfig.swift in Sources */, + 1C3A761025E4D6FB00415F32 /* ScheduleModelController.swift in Sources */, + 1C3A760025E4D5AE00415F32 /* UIViewExtension.swift in Sources */, + 1C3A75F725E4D2E300415F32 /* CalendarExtension.swift in Sources */, + 1C3A760D25E4D6CE00415F32 /* CalenderCellVC.swift in Sources */, 84BDC8D625D64FD200144860 /* Memo.swift in Sources */, + 1C3A761C25E4D81C00415F32 /* SingleCalendarVC.swift in Sources */, + 1C3A761925E4D77100415F32 /* NotificationController.swift in Sources */, 84BDC8D325D64F5B00144860 /* MemoListTableViewCell.swift in Sources */, + CC99BC5125E4DC1600F2CCC7 /* WebViewController.swift in Sources */, + CC99BC3125E4DBD200F2CCC7 /* ScheduleCalendarVC.swift in Sources */, + CC99BC5825E4DCF200F2CCC7 /* ViewControllerExtension.swift in Sources */, 1CA2BC8425D249FB0044EA31 /* MenuViewController.swift in Sources */, - 1CE8AF1525D25182004A8F37 /* ScheduleViewController.swift in Sources */, + CC99BC5A25E4DCF200F2CCC7 /* URLRequestExtension.swift in Sources */, + CC99BC5B25E4DCF200F2CCC7 /* Loopable.swift in Sources */, + CC99BC4F25E4DC1600F2CCC7 /* StringExtension.swift in Sources */, + CC99BC4E25E4DC1600F2CCC7 /* IcsElement.swift in Sources */, + CC99BC4A25E4DC1600F2CCC7 /* ICalendarAlarm.swift in Sources */, + 1C3A761325E4D72400415F32 /* Schedule.swift in Sources */, + CC99BC4525E4DC1600F2CCC7 /* ConnectGoogleCalendarVC.swift in Sources */, + 1CE8AF1525D25182004A8F37 /* ScheduleCollectionVC.swift in Sources */, + CC99BC4D25E4DC1600F2CCC7 /* ICalendarParser.swift in Sources */, + 1C3A75FD25E4D57700415F32 /* ScrollViewExtension.swift in Sources */, 1CA2BC8825D24A6E0044EA31 /* MenuTableViewCell.swift in Sources */, + CC99BC4C25E4DC1600F2CCC7 /* ICalendarEvent.swift in Sources */, 1C3E985425DAABFE00268DB0 /* DetailMemoViewController.swift in Sources */, + 1C3A760A25E4D69E00415F32 /* CellContentsView.swift in Sources */, + CC99BC5025E4DC1600F2CCC7 /* OAuthGather.swift in Sources */, + CC99BC4925E4DC1600F2CCC7 /* ICalendar.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -352,6 +544,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Schedule_B/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -370,6 +563,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Schedule_B/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/Schedule_B.xcodeproj/project.xcworkspace/xcuserdata/keen.xcuserdatad/UserInterfaceState.xcuserstate b/Schedule_B.xcodeproj/project.xcworkspace/xcuserdata/keen.xcuserdatad/UserInterfaceState.xcuserstate index b4e1e54..01e7463 100644 Binary files a/Schedule_B.xcodeproj/project.xcworkspace/xcuserdata/keen.xcuserdatad/UserInterfaceState.xcuserstate and b/Schedule_B.xcodeproj/project.xcworkspace/xcuserdata/keen.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Schedule_B.xcodeproj/xcuserdata/keen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Schedule_B.xcodeproj/xcuserdata/keen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..6c8bf4d --- /dev/null +++ b/Schedule_B.xcodeproj/xcuserdata/keen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/Schedule_B/Assets.xcassets/google_calendar.imageset/Contents.json b/Schedule_B/Assets.xcassets/google_calendar.imageset/Contents.json new file mode 100644 index 0000000..240f5f4 --- /dev/null +++ b/Schedule_B/Assets.xcassets/google_calendar.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "google_calendar.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule_B/Assets.xcassets/google_calendar.imageset/google_calendar.png b/Schedule_B/Assets.xcassets/google_calendar.imageset/google_calendar.png new file mode 100644 index 0000000..305dd4a Binary files /dev/null and b/Schedule_B/Assets.xcassets/google_calendar.imageset/google_calendar.png differ diff --git a/Schedule_B/Assets.xcassets/sign_with_google.imageset/Contents.json b/Schedule_B/Assets.xcassets/sign_with_google.imageset/Contents.json new file mode 100644 index 0000000..ebea429 --- /dev/null +++ b/Schedule_B/Assets.xcassets/sign_with_google.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "googleLogin.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule_B/Assets.xcassets/sign_with_google.imageset/googleLogin.png b/Schedule_B/Assets.xcassets/sign_with_google.imageset/googleLogin.png new file mode 100644 index 0000000..d9986fa Binary files /dev/null and b/Schedule_B/Assets.xcassets/sign_with_google.imageset/googleLogin.png differ diff --git a/Schedule_B/Base.lproj/Main.storyboard b/Schedule_B/Base.lproj/Main.storyboard index 430a692..d7987a5 100644 --- a/Schedule_B/Base.lproj/Main.storyboard +++ b/Schedule_B/Base.lproj/Main.storyboard @@ -4,8 +4,10 @@ + + @@ -55,7 +57,7 @@ - + @@ -77,7 +79,7 @@ - + @@ -107,70 +109,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -194,6 +138,9 @@ + + + @@ -210,6 +157,9 @@ + + + @@ -233,23 +183,29 @@ - + + + + + + + - + + - + - - + @@ -307,7 +263,7 @@ - + @@ -323,6 +279,36 @@ + + + + + + + + + + + + + + + + + + + + + @@ -346,7 +332,7 @@ - + @@ -367,37 +353,541 @@ - + - + - - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + diff --git a/Schedule_B/CalendarExtension.swift b/Schedule_B/CalendarExtension.swift new file mode 100644 index 0000000..b5a0e0a --- /dev/null +++ b/Schedule_B/CalendarExtension.swift @@ -0,0 +1,92 @@ +// +// CalendarExtension.swift +// Schedule_B +// +// Created by KEEN on 2021/02/23. +// + +import Foundation + +extension Calendar { + static func getDaysInMonth(_ yearAndMonth: Int) -> Int { + let firstDate = firstDateOfMonth(yearAndMonth) + let range = Calendar.current.range( + of: .day, + in: .month, + for: firstDate) + return range!.count + } + static func firstDateOfMonth(_ yearAndMonth: Int) -> Date { + return DateComponents( + calendar: Calendar.current, + timeZone: .current, + year: yearAndMonth / 100, + month: yearAndMonth % 100).date! + } +} + +extension Date { + var year: Int { + Calendar.current.component(.year, from: self) + } + var month: Int { + Calendar.current.component(.month, from: self) + } + var day: Int { + Calendar.current.component(.day, from: self) + } + var weekDay: Int { + Calendar.current.component(.weekday, from: self) + } + var hour: Int { + Calendar.current.component(.hour, from: self) + } + var minute: Int { + Calendar.current.component(.minute, from: self) + } + + var aMonthAgo: Date { + return Calendar.current.date( + byAdding: DateComponents(month: -1), to: self)! + } + var aMonthAfter: Date { + return Calendar.current.date( + byAdding: DateComponents(month: 1), to: self)! + } + func isSameDay(with toCompare: Date) -> Bool { + return self.year == toCompare.year && self.month == toCompare.month && self.day == toCompare.day + } + var toInt: Int { + return (self.year * 10000) + (self.month * 100) + self.day + } + func getNext(by component: ComponentType) -> Date{ + var nextComponent = Calendar.current.dateComponents([.hour, .minute], from: self) + + switch component { + case .day(let day): + nextComponent.day = day + case .weekday(let weekday): + nextComponent.weekday = weekday + } + + return Calendar.current.nextDate( + after: self, + matching: nextComponent, + matchingPolicy: .nextTime)! + } + enum ComponentType { + case day (Int) + case weekday (Int) + } +} + +extension Int { + var toDate: Date?{ + DateComponents( + calendar: Calendar.current, + timeZone: .current, + year: self / 10000, + month: (self / 100) % 100, + day: self % 100).date + } +} diff --git a/Schedule_B/CalenderCellVC.swift b/Schedule_B/CalenderCellVC.swift new file mode 100644 index 0000000..86c290f --- /dev/null +++ b/Schedule_B/CalenderCellVC.swift @@ -0,0 +1,28 @@ +// +// CalenderCellVC.swift +// Schedule_B +// +// Created by KEEN on 2021/02/23. +// + +import SwiftUI +import Combine + +class CalendarCellVC: UICollectionViewCell { + var cellView: CellContentsView { + get { + hostingController.rootView + } + set{ + hostingController.rootView = newValue + } + } + + + // Swift UI + var hostingController = UIHostingController(rootView: CellContentsView()) + static func size(in frameSize: CGSize) -> CGSize{ + CGSize(width: ((frameSize.width - 2) / 8), + height: ((frameSize.height - 2) / 7)) + } +} diff --git a/Schedule_B/CellContentsView.swift b/Schedule_B/CellContentsView.swift new file mode 100644 index 0000000..5b727e8 --- /dev/null +++ b/Schedule_B/CellContentsView.swift @@ -0,0 +1,96 @@ +// +// CellContentsView.swift +// Schedule_B +// +// Created by KEEN on 2021/02/23. +// + +import SwiftUI + +struct CellContentsView: View { + var date: Int? + var isToday = false + var schedules = [Schedule]() + var searchRequest: SingleCalendarViewController.SearchRequest? + var holiday: HolidayGather.Holiday? + private var filteredSchedules: Array.SubSequence { + let filtered = schedules.filter() { + if let search = searchRequest { + if search.priority != nil, search.priority != $0.priority{ + return false + } + if search.text != nil{ + return $0.title.lowercased().contains(search.text!) || $0.description.lowercased().contains(search.text!) + }else { + return true + } + }else { + return true + } + } + // fillter by priority + return filtered.sorted { lhs, rhs in + if lhs.priority == rhs.priority { + return lhs > rhs + }else { + return lhs.priority < rhs.priority + } + }.prefix(3) + } + private var dateFontColor: Color { + if holiday != nil { + if date!.toDate!.weekDay == 1 || holiday!.type == .national { + return Color.red + }else if date!.toDate!.weekDay == 7 { + return Color.blue + }else { + return Color.gray + } + }else { + return Color.forDate(date!.toDate!) + } + } + + var body: some View { + if date != nil { + GeometryReader{ geometry in + VStack{ + Text(String(date! % 100)) + .font(.body) + .overlay(isToday ? RoundedRectangle( + cornerRadius: 10) + .stroke(Color.red.opacity(0.8), + lineWidth: 2.5) + : nil) + if holiday != nil{ + Text(holiday!.title) + .font(.system(size: 10)) + .lineLimit(1) + } + } + .foregroundColor(dateFontColor) + .padding(3) + .position(x: geometry.size.width / 2, y: geometry.size.height * 0.2) + VStack{ + ForEach(filteredSchedules, id:\.id) { schedule in + HStack{ + RoundedRectangle(cornerRadius: 10, style: .circular) + .inset(by: CGFloat(4.5 - Double(schedules.count))) + .fill(Color.byPriority(schedule.priority)) + .aspectRatio(0.3, contentMode: .fit) + Text(schedule.title) + .font(.system(size: 10)) + .lineLimit(1) + .foregroundColor(Color.byPriority(schedule.priority)) + .padding(.leading, -7) + } + .frame(width: geometry.size.width, + height: + (geometry.size.height * 0.7) / CGFloat(schedules.count + 1)) + } + } + .padding(.top, holiday == nil ? 25: 35) + } + } + } +} diff --git a/Schedule_B/ColorExtension.swift b/Schedule_B/ColorExtension.swift new file mode 100644 index 0000000..0882c2f --- /dev/null +++ b/Schedule_B/ColorExtension.swift @@ -0,0 +1,49 @@ +// +// ColorExtension.swift +// Schedule_B +// +// Created by KEEN on 2021/02/23. +// + +import SwiftUI + +extension CGColor { + static var salmon: CGColor { + CGColor(red: 0.98, green: 0.52, blue: 0.55, alpha: 0.8) + } +} +extension Color { + static func forDate(_ date: Date) -> Color { + if date.weekDay == 1 { + return Color.pink + }else if date.weekDay == 7 { + return Color.blue + }else { + return Color.black + } + } + enum Button: String, CaseIterable { + case red = "🔴" + case orange = "🟠" + case green = "🟢" + case blue = "🔵" + case black = "⚫️" + } + static func byPriority(_ priority: Int) -> Color { + guard priority > 0 , priority < 6 else { + fatalError("Drawing fail: Invalid priority of schedule is passed to cell") + } + switch priority { + case 1: + return Color.red + case 2: + return Color.orange + case 3: + return Color.green + case 4: + return Color.blue + default: + return Color.black + } + } +} diff --git a/Schedule_B/DetailMemoViewController.swift b/Schedule_B/DetailMemoViewController.swift index fe6a444..9f89c71 100644 --- a/Schedule_B/DetailMemoViewController.swift +++ b/Schedule_B/DetailMemoViewController.swift @@ -8,49 +8,93 @@ import UIKit class DetailMemoViewController: UIViewController { - - var memo: Memo? - // TODO: 메모의 내용을 입력하지 않을 시 저장이 되지 않고 alert문을 띄우도록 할 것. + var memoType: Memo? + + // TODO: 메모 수정 후 저장 버튼 누를 때, dismiss 안 되는 것. + // TODO: 수정 후 저장을 누르면, 메모가 수정되는 것이 아니라 새로운 메모가 추가된다. update 함수 구현하기. @IBOutlet weak var titleLabel: UITextField! @IBOutlet weak var contentsTextView: UITextView! @IBOutlet weak var saveButton: UIBarButtonItem! + @IBOutlet weak var cancelButton: UIBarButtonItem! + + // MARK: 변경된 메모 타입을 dirayVC에 전달. + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if let vc = segue.destination.children.first as? DiaryViewController { + vc.editMemoType = memoType + } + } override func viewDidLoad() { super.viewDidLoad() titleLabel.delegate = self - if let memo = memo { + if let memo = memoType { navigationItem.title = "메모 수정" + titleLabel.text = memo.mainText contentsTextView.text = memo.contentText } - - updateSaveButtonState() - } +} - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - - guard let button = sender as? UIBarButtonItem, button == saveButton else { return } - - let title = titleLabel.text ?? "" + func update(memo: Memo) { + titleLabel.text = memo.mainText + contentsTextView.text = memo.contentText + } + + // MARK: 저장 버튼 action + @IBAction func onSave(sender: UIBarButtonItem) { + let title = titleLabel.text let contents = contentsTextView.text ?? "" + + if title == "" { + showAlert("제목을 입력해주세요") + return + } else { + // 새로운 메모를 추가할 때 + let newMemo = Memo(mainText: title!, contentText: contents) + Memo.dummyMemoList.append(newMemo) + dismiss(animated: true, completion: nil) + } - memo = Memo(mainText: title, contentText: contents) + // 메모 수정 상태일 때 + if let memo = memoType { + update(memo: memo) + dismiss(animated: true, completion: nil) + } } - // TODO: 저장 버튼을 누르면 DiaryVC로 전환되며 메모가 추가되도록. + // MARK: 취소 버튼 action + @IBAction func cancelAction(_ sender: Any) { + let addMode = presentingViewController is UINavigationController - @IBAction func saveAction(_ sender: Any) { + if addMode { + dismiss(animated: true, completion: nil) + } else if let ownedNVC = navigationController { + ownedNVC.popViewController(animated: true) + } else { + fatalError("detailMemoVC가 navigation controller 안에 없습니다.") + } + } + + // MARK: Configuring Alert + func showAlert(_ message: String) { + let alert = UIAlertController( + title: .none, + message: message, + preferredStyle: .alert) + let offAction = UIAlertAction(title: "확인", style: .default) + alert.addAction(offAction) + self.present(alert, animated: true) } } +// MARK: textFieldDelegate extension DetailMemoViewController: UITextFieldDelegate { func textFieldDidBeginEditing(_ textField: UITextField) { - saveButton.isEnabled = false + saveButton.isEnabled = true } func textFieldShouldReturn(_ textField: UITextField) -> Bool { @@ -60,12 +104,6 @@ extension DetailMemoViewController: UITextFieldDelegate { } func textFieldDidEndEditing(_ textField: UITextField) { - updateSaveButtonState() navigationItem.title = textField.text } - - private func updateSaveButtonState() { - let text = titleLabel.text ?? "" - saveButton.isEnabled = !text.isEmpty - } } diff --git a/Schedule_B/DiaryViewController.swift b/Schedule_B/DiaryViewController.swift index ab225aa..0436519 100644 --- a/Schedule_B/DiaryViewController.swift +++ b/Schedule_B/DiaryViewController.swift @@ -8,13 +8,15 @@ import UIKit import FSCalendar +// TODO: 메모 수정 시, 새로운 메모로 추가 저장되는 부분 구현. + class DiaryViewController: UIViewController { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var diaryCalendarView: FSCalendar! - var memos = [Memo]() - + var editMemoType: Memo? + let formatter : DateFormatter = { let f = DateFormatter() f.dateStyle = .short @@ -23,6 +25,11 @@ class DiaryViewController: UIViewController { return f }() + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableView.reloadData() + } + override func viewDidLoad() { super.viewDidLoad() @@ -32,42 +39,68 @@ class DiaryViewController: UIViewController { diaryCalendarView.scrollEnabled = true diaryCalendarView.scrollDirection = .vertical + } + + // MARK: 메모 cell 선택 시, detailVC에 데이터 연동하기 + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + guard let vc = segue.destination as? DetailMemoViewController else { return } + guard let selectedMemoCell = sender as? MemoListTableViewCell else { return } + guard let indexPath = tableView.indexPath(for: selectedMemoCell) else { return } - laodDummyData() + let selectedMemo = Memo.dummyMemoList[indexPath.row] + vc.memoType = selectedMemo } + // MARK: 메모 생성 및 편집 @IBAction func unwindToMemoList(sender: UIStoryboardSegue) { - if let sourceVC = sender.source as? DetailMemoViewController, let memo = sourceVC.memo { - let newMemo = IndexPath(row: memos.count, section: 0) - memos.append(memo) - tableView.insertRows(at: [newMemo], with: .automatic) + if let sourceVC = sender.source as? DetailMemoViewController, let memo = sourceVC.memoType { + if let selectedIndexPath = tableView.indexPathForSelectedRow { + // 존재하는 메모 업데이트 + Memo.dummyMemoList[selectedIndexPath.row] = memo + tableView.reloadRows(at: [selectedIndexPath], with: .none) + } else { + // 새로운 메모 추가 + let newMemoIndexPath = IndexPath(row: Memo.dummyMemoList.count, section: 0) + Memo.dummyMemoList.append(memo) + tableView.insertRows(at: [newMemoIndexPath], with: .automatic) + } } } - - func laodDummyData() { - let memo1 = Memo(mainText: "메모1", contentText: "메모1 detail") - let memo2 = Memo(mainText: "메모2", contentText: "메모2 detail") - let memo3 = Memo(mainText: "메모3", contentText: "메모3 detail") - - memos += [memo1, memo2, memo3] - } } + + +// +// MARK: Extension Delegate & DataSource + extension DiaryViewController: UITableViewDelegate{ + // MARK: 메모 삭제 관련 코드 + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + if editingStyle == .delete { + Memo.dummyMemoList.remove(at: indexPath.row) + tableView.deleteRows(at: [indexPath], with: .fade) + } else if editingStyle == .insert { + } + } } extension DiaryViewController: UITableViewDataSource{ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return memos.count + return Memo.dummyMemoList.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "memoCell", for: indexPath) as! MemoListTableViewCell - let memo = memos[indexPath.row] - cell.mainText.text = memo.mainText - cell.subText.text = formatter.string(from: memo.subText) + if let exitMemo = editMemoType { + navigationItem.title = "메모 편집" + cell.mainText.text = exitMemo.mainText + cell.subText.text = formatter.string(from: exitMemo.subText) + } else { + let memo = Memo.dummyMemoList[indexPath.row] + cell.mainText.text = memo.mainText + cell.subText.text = formatter.string(from: memo.subText) + } return cell } } - diff --git a/Schedule_B/Future&Promise.swift b/Schedule_B/Future&Promise.swift new file mode 100644 index 0000000..cddaee5 --- /dev/null +++ b/Schedule_B/Future&Promise.swift @@ -0,0 +1,74 @@ +// +// Future.swift +// Schedule_B +// +// Created by Shin on 2/20/21. +// + +import Foundation + +class Future { + typealias Result = Swift.Result + + fileprivate var result: Result? { + didSet { result.map(report) } + } + private var callbacks = [(Result) -> Void]() + + func observe(using callback: @escaping (Result) -> Void) { + if let result = result { + return callback(result) + } + callbacks.append(callback) + } + private func report(result: Result) { + callbacks.forEach { $0(result) } + callbacks = [] + } + + func chained(using closure: @escaping (Value) throws -> Future) -> Future { + let promise = Promise() + + observe { result in + switch result { + case .success(let value): + do { + let future = try closure(value) + + future.observe { result in + switch result{ + case .success(let value): + promise.resolve(with: value) + case .failure(let error): + promise.reject(with: error) + } + } + }catch { + promise.reject(with: error) + } + case .failure(let error): + promise.reject(with: error) + } + } + return promise + } + func transFormed (with closure: @escaping (Value) throws -> T) -> Future { + chained { value in + try Promise(value: closure(value)) + } + } +} + + +class Promise: Future { + init(value: Value? = nil) { + super.init() + result = value.map(Result.success) + } + func resolve(with value: Value) { + result = .success(value) + } + func reject(with error: Error) { + result = .failure(error) + } +} diff --git a/Schedule_B/GoogleCalendar/ConnectGoogleCalendarVC.swift b/Schedule_B/GoogleCalendar/ConnectGoogleCalendarVC.swift new file mode 100644 index 0000000..8f6b7d5 --- /dev/null +++ b/Schedule_B/GoogleCalendar/ConnectGoogleCalendarVC.swift @@ -0,0 +1,153 @@ +// +// ConnectGoogleCalendarVC.swift +// Schedule_B +// +// Created by Shin on 2/22/21. +// + +import UIKit + +class ConnectGoogleCalendarVC: UITableViewController { + + // MARK: Controller + private var googleOAuthGather: OAuthGather! + private var requestAPIScope: GoogleOAuthConfig.CalendarAPIScope = .calendarReadAndWrite + + // MARK:- Properties + private var tokenData: OAuthGather.TokenData? + @IBOutlet weak var calendarIDInput: UITextField! + @IBOutlet weak var loginButton: UIButton! + @IBOutlet weak var loggedInLabel: UILabel! + @IBOutlet weak var scopeController: UISegmentedControl! + + // MARK:- User Intents + + @IBAction func scopeController(_ sender: UISegmentedControl) { + requestAPIScope = GoogleOAuthConfig.CalendarAPIScope(rawValue: sender.selectedSegmentIndex)! + } + + @IBAction func tapAddCalendarButton(_ sender: UIButton) { + + guard tokenData != nil else { + let alertController = UIAlertController( + title: "로그인이 필요합니다", + message: "구글 계정으로 로그인 해주세요", + preferredStyle: .alert) + let dismissAction = UIAlertAction( + title: "확인", style: .cancel) + alertController.addAction(dismissAction) + present(alertController, animated: true) + return + } + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + guard let userInputID = calendarIDInput.text , emailPred.evaluate(with: userInputID) else { + let alertController = UIAlertController( + title: "캘린더 ID를 확인해주세요", + message: "이메일 주소를 입력해주세요 기본 캘린더는 사용자의 구글 이메일 주소입니다", + preferredStyle: .alert) + let dismissAction = UIAlertAction( + title: "확인", style: .cancel) + alertController.addAction(dismissAction) + present(alertController, animated: true) + return + } + requestCalendarData(with: userInputID) + } + + private func requestCalendarData(with calendarID: String) { + let urlString = "https://apidata.googleusercontent.com/caldav/v2/\(calendarID)/events" + var request = URLRequest(url: URL(string: urlString)!) + request.httpMethod = "GET" + request.addValue("\(tokenData!.token_type) \(tokenData!.access_token)", forHTTPHeaderField: "Authorization") + let promise = request.sendWithPromise() + promise.observe { [weak weakSelf = self] result in + switch result { + case .success(let data): + weakSelf?.parseCalendarData(data) + case .failure(let error): + print("error: ", error.localizedDescription) + } + } + } + /// Not implemented yet + private func parseCalendarData(_ data: Data) { +// let parsed = ICalendar.load(string: String(data: data, encoding: .utf8)!) +// var schedules = [Schedule]() +// for calendar in parsed { +// calendar.subComponents.forEach { +// if let event = $0 as? ICalendarEvent { +// print("event: \(event)") +// let scheduleDate: Schedule.DateType +// if let startDate = event.dtstart, +// let endDate = event.dtend { +// scheduleDate = .period(start: startDate, end: endDate) +// }else { +// scheduleDate = .spot(event.dtstamp) +// } +// let alarm: Schedule.Alarm? +// let alarmAtrributes: [String: String] +// if let iCalendarAlarm = event.subComponents.first(where: { $0 is ICalendarAlarm }) as? ICalendarAlarm { +// alarmAtrributes = iCalendarAlarm.otherAttrs +// +// guard let trigger = alarmAtrributes["TRIGGER"] else { +// fatalError("iCalendar event: \(event) has alarm with out trigger\n \(alarmAtrributes)") +// } +// let isAhead = trigger.first! == "-" +// let dayTo: Int? = trigger.contains("D") ? 0: 0 +// }else { +// alarm = nil +// } +// } +// } +// print("other attributes ----------") +// print(calendar.otherAttrs) +// } + } + + @IBAction func tapLoginButton(_ sender: Any) { + let webViewController = WebViewController() + let showAlert: (Error)-> Void = { [weak weakSelf = self] error in + let error = error as NSError + let alertContoller = UIAlertController( + title: "연동 실패", + message: error.code == 1 ? "사용자가 연결을 취소하였습니다": + "알 수 없는 오류가 발생했습니다", + preferredStyle: .alert) + print(error.localizedDescription) + let dissmissAction = UIAlertAction( + title: "확인", style: .cancel) + alertContoller.addAction(dissmissAction) + webViewController.dismiss(animated: true) { + weakSelf?.present(alertContoller, animated: true) + } + } + googleOAuthGather = OAuthGather( + with: GoogleOAuthConfig(for: requestAPIScope), + drawTo: self, errorHandling: showAlert) + + present(webViewController, animated: true) + googleOAuthGather.tokenPromise.observe { [weak weakSelf = self] result in + switch result { + case .success(let data): + DispatchQueue.main.sync { + webViewController.webView.stopLoading() + webViewController.dismiss(animated: true) + weakSelf?.loginButton.isEnabled = false + weakSelf?.scopeController.isEnabled = false + weakSelf?.loggedInLabel.isHidden = false + } + weakSelf?.tokenData = data + case .failure(let error): + print("Connect cancled") + print(error) + showAlert(error) + } + } + } + override func viewDidLoad() { + super.viewDidLoad() + loginButton.adjustsImageWhenDisabled = true + } + +} diff --git a/Schedule_B/GoogleCalendar/GoogleOAuthConfig.swift b/Schedule_B/GoogleCalendar/GoogleOAuthConfig.swift new file mode 100644 index 0000000..f18efa0 --- /dev/null +++ b/Schedule_B/GoogleCalendar/GoogleOAuthConfig.swift @@ -0,0 +1,113 @@ +// +// GoogleOAuth.swift +// Schedule_B +// +// Created by Shin on 2/19/21. +// + +import Foundation +import CommonCrypto + +struct GoogleOAuthConfig { + let requestScope: CalendarAPIScope + private let clientID = "285345924946-19dlip45paennomcrq6o82m194codj8p.apps.googleusercontent.com" + let callbackURLScheme = "com.googleusercontent.apps.285345924946-19dlip45paennomcrq6o82m194codj8p" + private var redirectURI: String { + callbackURLScheme + ":/oauth2redirect" + } + private let pkce = PKCE() + func checkStateIsValid(_ state: String) -> Bool { + state == pkce.state + } + var handShakeUrl: URL { + var components = URLComponents() + components.scheme = "https" + components.host = "accounts.google.com" + components.path = "/o/oauth2/v2/auth" + components.queryItems = [ + "scope": requestScope.url, + "response_type": "code", + "redirect_uri": redirectURI, + "code_challenge": pkce.codeChallenage, + "code_challenge_method": pkce.codeChallengeMethod, + "state": pkce.state, + "client_id": clientID + ].map { URLQueryItem(name: $0, value: $1)} + return components.url! + } + func makeRequest(by code: String)-> URLRequest { + var components = URLComponents() + components.scheme = "https" + components.host = "oauth2.googleapis.com" + components.path = "/token" + var request = URLRequest(url: components.url!) + request.httpMethod = "POST" + let parameters = [ + "code": code, + "client_id": clientID, + "code_verifier": pkce.codeVerifier, + "redirect_uri": redirectURI, + "grant_type": "authorization_code", + ] + guard let body = try? JSONSerialization.data(withJSONObject: parameters, options: []) else { + fatalError("Fail to make http request \n \(parameters)") + } + request.httpBody = body + request.setValue("Host", forHTTPHeaderField: "oauth2.googleapis.com") + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + return request + } + init(for scope: CalendarAPIScope) { + requestScope = scope + } + + enum CalendarAPIScope: Int { + case calendarReadAndWrite + case calendarReadOnly + case eventReadAndWrite + case eventReadOnly + + var url: String { + switch self { + case .calendarReadAndWrite: + return "https://www.googleapis.com/auth/calendar" + case .calendarReadOnly: + return "https://www.googleapis.com/auth/calendar.readonly" + case .eventReadAndWrite: + return "https://www.googleapis.com/auth/calendar.events" + case .eventReadOnly: + return "https://www.googleapis.com/auth/calendar.events.readonly" + } + } + } + + private struct PKCE { + let codeVerifier: String + let codeChallenage: String + let codeChallengeMethod = "S256" + let state: String + static func trim(_ str: String) -> String { + str.replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + .trimmingCharacters(in: .whitespaces) + } + init() { + var buffer = [UInt8](repeating: 0, count: 32) + + _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer) + + codeVerifier = PKCE.trim(Data(buffer).base64EncodedString()) + guard let data = codeVerifier.data(using: .ascii) else { fatalError("Fail to create PKCE instance") } + buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &buffer) + } + let hash = Data(buffer) + codeChallenage = PKCE.trim(hash.base64EncodedString()) + buffer = [UInt8](repeating: 0, count: 32) + _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer) + state = PKCE.trim(Data(buffer).base64EncodedString()) + } + } +} diff --git a/Schedule_B/GoogleCalendar/ICalendarLibrary/CalendarComponent.swift b/Schedule_B/GoogleCalendar/ICalendarLibrary/CalendarComponent.swift new file mode 100644 index 0000000..65250d1 --- /dev/null +++ b/Schedule_B/GoogleCalendar/ICalendarLibrary/CalendarComponent.swift @@ -0,0 +1,7 @@ +import Foundation + +/// TODO add documentation +protocol CalendarComponent { + /// TODO add documentation + func toCal() -> String +} diff --git a/Schedule_B/GoogleCalendar/ICalendarLibrary/DateExtension.swift b/Schedule_B/GoogleCalendar/ICalendarLibrary/DateExtension.swift new file mode 100644 index 0000000..f6fba6a --- /dev/null +++ b/Schedule_B/GoogleCalendar/ICalendarLibrary/DateExtension.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Date { + /// Convert String to Date + func toIcalendarString() -> String { + return ICalendar.dateFormatter.string(from: self) + } +} diff --git a/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendar.swift b/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendar.swift new file mode 100644 index 0000000..9cba78f --- /dev/null +++ b/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendar.swift @@ -0,0 +1,87 @@ +import Foundation + +/// TODO add documentation +struct ICalendar { + var subComponents: [CalendarComponent] = [] + var otherAttrs = [String:String]() + + init(withComponents components: [CalendarComponent]?) { + if let components = components { + self.subComponents = components + } + } + + /// Loads the content of a given string. + /// + /// - Parameter string: string to load + /// - Returns: List of containted `Calendar`s + static func load(string: String) -> [ICalendar] { + let icsContent = string.components(separatedBy: "\r\n") + return parse(icsContent) + } + + /// Loads the contents of a given URL. Be it from a local path or external resource. + /// + /// - Parameters: + /// - url: URL to load + /// - encoding: Encoding to use when reading data, defaults to UTF-8 + /// - Returns: List of contained `Calendar`s. + /// - Throws: Error encountered during loading of URL or decoding of data. + /// - Warning: This is a **synchronous** operation! Use `load(string:)` and fetch your data beforehand for async handling. + static func load(url: URL, encoding: String.Encoding = .utf8) throws -> [ICalendar] { + let data = try Data(contentsOf: url) + guard let string = String(data: data, encoding: encoding) else { throw ICalendarError.encoding } + return load(string: string) + } + + private static func parse(_ icsContent: [String]) -> [ICalendar] { + let parser = ICalendarParser(icsContent) + do { + return try parser.read() + } catch let error { + print(error) + return [] + } + } + + static let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" + return dateFormatter + }() +} + +extension ICalendar: IcsElement { + + mutating func append(component: CalendarComponent?) { + guard let component = component else { + return + } + self.subComponents.append(component) + } + + mutating func addAttribute(attr: String, _ value: String) { + switch attr { // TODO switch not needed, it'll always be default + default: + otherAttrs[attr] = value + } + } + +} + +extension ICalendar: CalendarComponent { + func toCal() -> String { + var str = "BEGIN:VCALENDAR\n" + + for (key, val) in otherAttrs { + str += "\(key):\(val)\n" + } + + for component in subComponents { + str += "\(component.toCal())\n" + } + + str += "END:VCALENDAR" + return str + } +} diff --git a/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarAlarm.swift b/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarAlarm.swift new file mode 100644 index 0000000..17730ce --- /dev/null +++ b/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarAlarm.swift @@ -0,0 +1,29 @@ +import Foundation + +/// TODO add documentation + struct ICalendarAlarm { + var subComponents: [CalendarComponent] = [] + var otherAttrs = [String:String]() +} + +extension ICalendarAlarm: IcsElement { + mutating func addAttribute(attr: String, _ value: String) { + switch attr { // TODO switch not needed, it'll always be default + default: + otherAttrs[attr] = value + } + } +} + +extension ICalendarAlarm: CalendarComponent { + func toCal() -> String { + var str = "BEGIN:VALARM\n" + + for (key, val) in otherAttrs { + str += "\(key):\(val)\n" + } + + str += "END:VALARM" + return str + } +} diff --git a/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarError.swift b/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarError.swift new file mode 100644 index 0000000..25c71f4 --- /dev/null +++ b/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarError.swift @@ -0,0 +1,8 @@ +import Foundation + +enum ICalendarError: Error { + case fileNotFound + case encoding + case parseError + case unsupportedICalVersion +} diff --git a/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarEvent.swift b/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarEvent.swift new file mode 100644 index 0000000..72acf6c --- /dev/null +++ b/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarEvent.swift @@ -0,0 +1,96 @@ +import Foundation + +/// TODO add documentation +struct ICalendarEvent: Loopable { + var subComponents: [CalendarComponent] = [] + var otherAttrs = [String:String]() + + // required + var uid: String! + var dtstamp: Date! + + // optional + // var organizer: Organizer? = nil + var location: String? + var summary: String? + var descr: String? + // var class: some enum type? + var dtstart: Date? + var dtend: Date? + + init(uid: String? = NSUUID().uuidString, dtstamp: Date? = Date()) { + self.uid = uid + self.dtstamp = dtstamp + } +} + +extension ICalendarEvent: CalendarComponent { + func toCal() -> String { + var str: String = "BEGIN:VEVENT\n" + + if let uid = uid { + str += "UID:\(uid)\n" + } + if let dtstamp = dtstamp { + str += "DTSTAMP:\(dtstamp.toIcalendarString())\n" + } + if let summary = summary { + str += "SUMMARY:\(summary)\n" + } + if let descr = descr { + str += "DESCRIPTION:\(descr)\n" + } + if let dtstart = dtstart { + str += "DTSTART:\(dtstart.toIcalendarString())\n" + } + if let dtend = dtend { + str += "DTEND:\(dtend.toIcalendarString())\n" + } + + for (key, val) in otherAttrs { + str += "\(key):\(val)\n" + } + + for component in subComponents { + str += "\(component.toCal())\n" + } + + str += "END:VEVENT" + return str + } +} + +extension ICalendarEvent: IcsElement { + mutating func addAttribute(attr: String, _ value: String) { + switch attr { + case "UID": + uid = value + case "DTSTAMP": + dtstamp = value.toICalendarDate() + case "DTSTART": + dtstart = value.toICalendarDate() + case "DTEND": + dtend = value.toICalendarDate() + // case "ORGANIZER": + // organizer + case "SUMMARY": + summary = value + case "DESCRIPTION": + descr = value + default: + otherAttrs[attr] = value + } + } +} + +extension ICalendarEvent: Equatable { } + + func ==(lhs: ICalendarEvent, rhs: ICalendarEvent) -> Bool { + return lhs.uid == rhs.uid +} + +extension ICalendarEvent: CustomStringConvertible { + var description: String { + return "\(dtstamp.toIcalendarString()): \(summary ?? "")" + } +} diff --git a/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarParser.swift b/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarParser.swift new file mode 100644 index 0000000..1d6f407 --- /dev/null +++ b/Schedule_B/GoogleCalendar/ICalendarLibrary/ICalendarParser.swift @@ -0,0 +1,75 @@ +import Foundation + +/// TODO add documentation +class ICalendarParser { + let icsContent: [String] + + init(_ ics: [String]) { + icsContent = ics + } + + func read() throws -> [ICalendar] { + var completeCal = [ICalendar?]() + + // Such state, much wow + var inCalendar = false + var currentCalendar: ICalendar? + var inEvent = false + var currentEvent: ICalendarEvent? + var inAlarm = false + var currentAlarm: ICalendarAlarm? + + for (_ , line) in icsContent.enumerated() { + switch line { + case "BEGIN:VCALENDAR": + inCalendar = true + currentCalendar = ICalendar(withComponents: nil) + continue + case "END:VCALENDAR": + inCalendar = false + completeCal.append(currentCalendar) + currentCalendar = nil + continue + case "BEGIN:VEVENT": + inEvent = true + currentEvent = ICalendarEvent() + continue + case "END:VEVENT": + inEvent = false + currentCalendar?.append(component: currentEvent) + currentEvent = nil + continue + case "BEGIN:VALARM": + inAlarm = true + currentAlarm = ICalendarAlarm() + continue + case "END:VALARM": + inAlarm = false + currentEvent?.append(component: currentAlarm) + currentAlarm = nil + continue + default: + break + } + + guard let (key, value) = line.toKeyValuePair(splittingOn: ":") else { +// print("(key, value) is nil") // DEBUG + continue + } + + if inCalendar && !inEvent { + currentCalendar?.addAttribute(attr: key, value) + } + + if inEvent && !inAlarm { + currentEvent?.addAttribute(attr: key, value) + } + + if inAlarm { + currentAlarm?.addAttribute(attr: key, value) + } + } + + return completeCal.compactMap{ $0 } + } +} diff --git a/Schedule_B/GoogleCalendar/ICalendarLibrary/IcsElement.swift b/Schedule_B/GoogleCalendar/ICalendarLibrary/IcsElement.swift new file mode 100644 index 0000000..19f4888 --- /dev/null +++ b/Schedule_B/GoogleCalendar/ICalendarLibrary/IcsElement.swift @@ -0,0 +1,20 @@ +import Foundation + +/// TODO add documentation + protocol IcsElement { + var subComponents: [CalendarComponent] { get set } + var otherAttrs: [String:String] { get set } + + /// TODO add documentation + mutating func addAttribute(attr: String, _ value: String) + /// TODO add documentation + mutating func append(component: CalendarComponent?) +} + +extension IcsElement { + mutating func append(component: CalendarComponent?) { + if let component = component { + subComponents.append(component) + } + } +} diff --git a/Schedule_B/GoogleCalendar/ICalendarLibrary/StringExtension.swift b/Schedule_B/GoogleCalendar/ICalendarLibrary/StringExtension.swift new file mode 100644 index 0000000..f2771bd --- /dev/null +++ b/Schedule_B/GoogleCalendar/ICalendarLibrary/StringExtension.swift @@ -0,0 +1,20 @@ +import Foundation + +extension String { + /// TODO add documentation + func toKeyValuePair(splittingOn separator: Character) -> (first: String, second: String)? { + let arr = self.split(separator: separator, + maxSplits: 1, + omittingEmptySubsequences: false) + if arr.count < 2 { + return nil + } else { + return (String(arr[0]), String(arr[1])) + } + } + + /// Convert String to Date + func toICalendarDate() -> Date? { + return ICalendar.dateFormatter.date(from: self) + } +} diff --git a/Schedule_B/GoogleCalendar/OAuthGather.swift b/Schedule_B/GoogleCalendar/OAuthGather.swift new file mode 100644 index 0000000..3b09498 --- /dev/null +++ b/Schedule_B/GoogleCalendar/OAuthGather.swift @@ -0,0 +1,73 @@ +// +// GoogleOAuthController.swift +// Schedule_B +// +// Created by Shin on 2/20/21. +// + +import Foundation +import WebKit +import AuthenticationServices + +struct OAuthGather { + private let handShakePromise: Promise + private let config: GoogleOAuthConfig + let tokenPromise: Future + + init(with config: GoogleOAuthConfig, drawTo presentationContextProvider: ASWebAuthenticationPresentationContextProviding, errorHandling: @escaping (Error) -> Void) { + let promise = Promise() + let session = ASWebAuthenticationSession(url: config.handShakeUrl, callbackURLScheme: config.callbackURLScheme) {callbackURL, error in + guard error == nil, let callbackURL = callbackURL else { + errorHandling(error!) + return + } + guard let state = URLComponents(string: callbackURL.absoluteString)?.queryItems?.first( + where: { $0.name == "state"} )?.value, config.checkStateIsValid(state) else { + fatalError("Received state is not valid") + } + guard let code = URLComponents(string: callbackURL.absoluteString)?.queryItems?.first(where: { $0.name == "code" + })?.value else { + let error = NSError(domain: ASWebAuthenticationSessionError.errorDomain, + code: 1, userInfo: nil) + DispatchQueue.main.sync { + errorHandling(error) + } + return + } + let request = config.makeRequest(by: code) + _ = request.sendWithPromise(promise) + } + self.config = config + + self.handShakePromise = promise + let tokenPromise = Promise() + self.tokenPromise = tokenPromise + promise.observe { result in + switch result { + case .success(let data): + do { + let decoder = JSONDecoder() + tokenPromise.resolve(with: try decoder.decode(TokenData.self, from: data)) + }catch { + errorHandling(error) + } + case .failure(let error): + errorHandling(error) + } + } + session.presentationContextProvider = presentationContextProvider + session.prefersEphemeralWebBrowserSession = true + session.start() + } + + struct TokenData: Codable { + let recievedAt = Date() + let expires_in: Int + let refresh_token: String + let access_token: String + let token_type: String + let scope: String + } +} + + diff --git a/Schedule_B/GoogleCalendar/WebViewController.swift b/Schedule_B/GoogleCalendar/WebViewController.swift new file mode 100644 index 0000000..d70fa86 --- /dev/null +++ b/Schedule_B/GoogleCalendar/WebViewController.swift @@ -0,0 +1,32 @@ +// +// webViewController.swift +// Schedule_B +// +// Created by Shin on 2/20/21. +// + +import WebKit +import AuthenticationServices + +class WebViewController: UIViewController, WKUIDelegate, WKNavigationDelegate { + var webView: WKWebView! + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let host = navigationAction.request.url?.host { + if host.contains("google") { + decisionHandler(.allow) + return + } + } + decisionHandler(.cancel) + } + override func loadView() { + let webConfiguration = WKWebViewConfiguration() + webView = WKWebView(frame: .zero, configuration: webConfiguration) + webView.uiDelegate = self + view = webView + } + override func viewDidLoad() { + super.viewDidLoad() + } +} diff --git a/Schedule_B/HolidayGather.swift b/Schedule_B/HolidayGather.swift new file mode 100644 index 0000000..6fe7177 --- /dev/null +++ b/Schedule_B/HolidayGather.swift @@ -0,0 +1,78 @@ +// +// HolidayGather.swift +// Schedule_B +// +// Created by KEEN on 2021/02/23. +// + +import Foundation + +struct HolidayGather { + private let apiKey = "9c57014c99e72d106d1fe7b23487390294256eb5" + private let urlEndpoint = "https://calendarific.com/api/v2/holidays?" + /** + Get national holidays from external api server [Calendarific](https://calendarific.com) + - parameter country: National Holiday to retrieve (US or Korea) + - parameter handler: Function handle data come from api sever ( work asynchronously) + */ + + struct Holiday: Codable { + let dateInt: Int + let title: String + let description: String + let type: HolidayType + + enum HolidayType: String, Codable { + case national = "National holiday" + case observance = "Observance" + case season = "Season" + case commonLocal = "Common local holiday" + } + init(from data: Response.HolidayCapsule){ + self.dateInt = (data.date.datetime["year"]! * 10000) + (data.date.datetime["month"]! * 100) + data.date.datetime["day"]! + self.title = data.name + self.description = data.description + self.type = HolidayType(rawValue: data.type[0]) ?? .national + } + } + + enum CountryCode: String{ + case korea = "KR" + case america = "US" + } + func retrieveHolidays(about year: Int, of country: CountryCode, handler: @escaping (Data) -> Void) { + + let urlString = urlEndpoint + "&api_key=" + apiKey + "&country=" + country.rawValue + "&year=" + String(year) + let url = URL(string: urlString)! + let task = URLSession.shared.dataTask(with: url) {(data, response, error) in + guard let data = data else { + print("Fail to get holiday data from server") + print("response: \(response.debugDescription)") + print("error: \(error.debugDescription)") + return } + handler(data) + } + task.resume() + } + + struct Response: Codable { + var meta: [String: Int] + var response: Data + + struct Data: Codable { + var holidays: [HolidayCapsule] + } + struct HolidayCapsule: Codable { + var name: String + var type: [String] + var description: String + var date: DateCapsule + + struct DateCapsule: Codable { + var iso: String + var datetime: [String: Int] + } + } + } + +} diff --git a/Schedule_B/Loopable.swift b/Schedule_B/Loopable.swift new file mode 100644 index 0000000..8c5c53e --- /dev/null +++ b/Schedule_B/Loopable.swift @@ -0,0 +1,36 @@ +// +// Loopable.swift +// Schedule_B +// +// Created by Shin on 2/23/21. +// + +import Foundation + +protocol Loopable { + func allProperties() throws -> [String: Any] +} + +extension Loopable { + func allProperties() throws -> [String: Any] { + + var result: [String: Any] = [:] + + let mirror = Mirror(reflecting: self) + + // Optional check to make sure we're iterating over a struct or class + guard let style = mirror.displayStyle, style == .struct || style == .class else { + throw NSError() + } + + for (property, value) in mirror.children { + guard let property = property else { + continue + } + + result[property] = value + } + + return result + } +} diff --git a/Schedule_B/Memo.swift b/Schedule_B/Memo.swift index bc92be9..5df49fa 100644 --- a/Schedule_B/Memo.swift +++ b/Schedule_B/Memo.swift @@ -16,7 +16,13 @@ class Memo{ init(mainText : String, contentText: String) { self.mainText = mainText self.contentText = contentText + subText = Date() - } + + static var dummyMemoList: [Memo] = [ + Memo(mainText: "dummy메모1", contentText: "dummy메모1 detail"), + Memo(mainText: "dummy메모2", contentText: "dummy메모2 detail"), + Memo(mainText: "dummy메모3", contentText: "dummy메모3 detail") + ] } diff --git a/Schedule_B/MemoListTableViewCell.swift b/Schedule_B/MemoListTableViewCell.swift index 15a75d0..dcb8ac4 100644 --- a/Schedule_B/MemoListTableViewCell.swift +++ b/Schedule_B/MemoListTableViewCell.swift @@ -10,5 +10,4 @@ import UIKit class MemoListTableViewCell: UITableViewCell { @IBOutlet weak var mainText: UILabel! @IBOutlet weak var subText: UILabel! - } diff --git a/Schedule_B/MenuViewController.swift b/Schedule_B/MenuViewController.swift index 9c87dab..1adad1d 100644 --- a/Schedule_B/MenuViewController.swift +++ b/Schedule_B/MenuViewController.swift @@ -9,7 +9,7 @@ import UIKit class MenuViewController: UIViewController { - var menuList: [String] = ["📘다이어리 캘린더", "📕스케쥴 캘린더"] + var menuList: [String] = ["📘다이어리 캘린더", "📕스케쥴 캘린더", "⚙️설정"] @IBOutlet weak var tableView: UITableView! @@ -17,6 +17,16 @@ class MenuViewController: UIViewController { super.viewDidLoad() self.tableView.rowHeight = CGFloat(60) } + var scheduleMC: ScheduleModelController! + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "scheduleVCSegue", + let scheduleCalendarVC = segue.destination as? ScheduleCalendarViewController{ + scheduleCalendarVC.modelController = scheduleMC + }else if segue.identifier == "settingVCSegue", + let settingVC = segue.destination as? SettingViewController { + settingVC.scheduleModelController = scheduleMC + } + } } extension MenuViewController: UITableViewDelegate { @@ -32,6 +42,7 @@ extension MenuViewController: UITableViewDataSource { switch indexPath.row { case 0: identifier = "MenuDiaryCell" case 1: identifier = "MenuScheduleCell" + case 2: identifier = "MenuSettingCell" default: identifier = "MenuDiaryCell" } diff --git a/Schedule_B/NotificationController.swift b/Schedule_B/NotificationController.swift new file mode 100644 index 0000000..3c48610 --- /dev/null +++ b/Schedule_B/NotificationController.swift @@ -0,0 +1,125 @@ +// +// NotificationController.swift +// Schedule_B +// +// Created by KEEN on 2021/02/23. +// + +import UserNotifications + +class NotificationController { + + private(set) var authorizationStatus: UNAuthorizationStatus = .notDetermined + + func setAlarm(of newSchedule: Schedule, numberOfRepeatForEach: Int = 5) { + guard let alarm = newSchedule.alarm else { + fatalError("Try to set alarm not existing \n \(newSchedule)") + } + var datesToAlarm = [Date]() + + switch alarm { + case .once(let date): + datesToAlarm.append(date) + case .periodic: + if case .cycle(let startDate, let factor, let values) = newSchedule.time { + switch factor { + case .weekday: + for weekday in values { + var dateToAlarm = startDate.getNext(by: .weekday(weekday)) + datesToAlarm.append(dateToAlarm) + for _ in 0.. Bool { + lhs.id == rhs.id + } + + static func < (lhs: Schedule, rhs: Schedule) -> Bool { + let criterion: (Date, Date) + switch (lhs.time, rhs.time) { + case(let .spot(dateLhs), let .spot(dateRhs)): + criterion = (dateLhs, dateRhs) + case (let .spot(dateLhs), let .period(startRhs, _)): + criterion = (dateLhs, startRhs) + case (let .spot(dateLhs), let .cycle(sinceRhs, _, _)): + criterion = (dateLhs, sinceRhs) + case (let .period(startLhs, _), let .period(startRhs, _)): + criterion = (startLhs, startRhs) + case (let .period(startLhs, _), let .spot(dateRhs)): + criterion = (startLhs, dateRhs) + case (let .period(startLhs, _), let .cycle(sinceRhs, _, _)): + criterion = (startLhs, sinceRhs) + case (let .cycle(sinceLhs, _, _), let .cycle(sinceRhs, _, _)): + criterion = (sinceLhs, sinceRhs) + case (let .cycle(sinceLhs, _, _), let .spot(dateRhs)): + criterion = (sinceLhs, dateRhs) + case (let .cycle(sinceLhs, _, _), let .period(startRhs, _)): + criterion = (sinceLhs, startRhs) + } + return criterion.0 < criterion.1 + } + init(title: String, description: String, priority: Int, time: DateType, alarm: Alarm?){ + self.title = title + self.description = description + self.priority = priority + self.time = time + if alarm != nil, alarm! == .periodic{ + if case .cycle = time{ + + }else { + fatalError("Invalid periodic alarm for non-cycle schedule") + } + } + self.alarm = alarm + } +} + +//Encode to JSON +extension Schedule.DateType { + + private enum CodingKeys: String, CodingKey { + case spot + case start + case end + case type + case cycleFactor + case cycleValues + } + + func encode(to encoder: Encoder) throws { + //access the keyed container + var container = encoder.container(keyedBy: CodingKeys.self) + + //iterate over self and encode (1) the status and (2) the associated value(s) + switch self { + case .spot(let date): + try container.encode("spot", forKey: .type) + try container.encode(date, forKey: .spot) + case .period(start: let start, end: let end): + try container.encode("period", forKey: .type) + try container.encode(start, forKey: .start) + try container.encode(end, forKey: .end) + case .cycle(since: let start, for: let cycleType, every: let values): + try container.encode("cycle", forKey: .type) + try container.encode(start, forKey: .start) + try container.encode(cycleType.rawValue, forKey: .cycleFactor) + try container.encode(values, forKey: .cycleValues) + } + } + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "spot": + let time = try container.decode(Date.self, forKey: .spot) + self = .spot(time) + case "period": + let start = try container.decode(Date.self, forKey: .start) + let end = try container.decode(Date.self, forKey: .end) + self = .period(start: start, end: end) + case "cycle": + let start = try container.decode(Date.self, forKey: .start) + let cycleType = try container.decode(Schedule.CycleFactor.self, forKey: .cycleFactor) + let values = try container.decode([Int].self, forKey: .cycleValues) + self = .cycle(since: start, for: cycleType, every: values) + default: + fatalError("Invalid Date type of schdule during decoding") + } + } +} + +extension Schedule.Alarm{ + private enum CodingKeys: String, CodingKey { + case type + case date + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .once(let dateToAlarm): + try container.encode("once", forKey: .type) + try container.encode(dateToAlarm, forKey: .date) + case .periodic: + try container.encode("periodic", forKey: .type) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(String.self, forKey: .type) + if type == "periodic" { + self = .periodic + } else { + let dateToAlarm = try container.decode(Date.self, forKey: .date) + self = .once(dateToAlarm) + } + } +} diff --git a/Schedule_B/ScheduleCalendarVC.swift b/Schedule_B/ScheduleCalendarVC.swift new file mode 100644 index 0000000..9f7049d --- /dev/null +++ b/Schedule_B/ScheduleCalendarVC.swift @@ -0,0 +1,183 @@ + + +import UIKit +import SwiftUI + +class ScheduleCalendarViewController: UIViewController, UIScrollViewDelegate, UISearchResultsUpdating, UISearchBarDelegate { + // MARK: Controllers + + weak var modelController: ScheduleModelController! + private let searchController = UISearchController() + lazy private var currentCalendarVC = SingleCalendarViewController( + of: currentCalendarView, + on: today.toInt / 100, + modelController: modelController, segue: performSegue) + lazy private var prevCalendarVC = SingleCalendarViewController( + of: prevCalendarView, + on: (today.aMonthAgo.toInt / 100), + modelController: modelController) + lazy private var nextCalendarVC = + SingleCalendarViewController( + of: nextCalendarView, + on:(today.aMonthAgo.toInt / 100), + modelController: modelController) + + // MARK:- Properties + + @IBOutlet weak var monthLabel: UILabel! + @IBOutlet weak var nextCalendarView: UICollectionView! + @IBOutlet weak var currentCalendarView: UICollectionView! + @IBOutlet weak var prevCalendarView: UICollectionView! + @IBOutlet weak var scrollCalendarView: UIScrollView! + @IBOutlet weak var datePickerModal: UIView! + + // MARK:- User intents + +// override func prepare(for segue: UIStoryboardSegue, sender: Any?) { +// if segue.identifier == "ShowDailyView", +// let dailyVC = segue.destination as? DailyViewController, +// let dateToShow = sender as? Int{ +// dailyVC.dateIntShowing = dateToShow +// dailyVC.modelController = modelController +// } +// } + @IBAction func tapSearchButton(_ sender: Any) { + if navigationItem.searchController == nil { + navigationItem.searchController = searchController + }else if !navigationItem.searchController!.isEditing { + navigationItem.searchController = nil + } + } + + func updateSearchResults(for searchController: UISearchController) { + let searchBar = searchController.searchBar + let searchText = searchBar.text == nil || searchBar.text!.isEmpty ? nil : searchBar.text! + let priority = searchBar.selectedScopeButtonIndex == 0 ? nil : searchBar.selectedScopeButtonIndex + currentCalendarVC.searchRequest.text = searchText?.lowercased() + currentCalendarVC.searchRequest.priority = priority + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollView.contentOffset.x > 30 && scrollView.contentOffset.x < 780 { + return + } + let firstDate = scrollView.contentOffset.x < 30 ? Calendar.firstDateOfMonth(prevCalendarVC.yearAndMonth) : + Calendar.firstDateOfMonth(nextCalendarVC.yearAndMonth) + + currentCalendarVC.yearAndMonth = (firstDate.year * 100) + firstDate.month + prevCalendarVC.yearAndMonth = (firstDate.aMonthAgo.year * 100) + firstDate.aMonthAgo.month + nextCalendarVC.yearAndMonth = (firstDate.aMonthAfter.year * 100) + firstDate.aMonthAfter.month + updateLabel(with: firstDate) + scrollView.scrollToChild(currentCalendarView) + } + + private var datePicker = UIDatePicker() + private(set) var selectedDate = Date() + @IBAction func showDatePicker(_ sender: UIButton) { + if datePickerModal.isHidden { + self.datePickerModal.isHidden = false + let blurView = UIVisualEffectView(effect: + UIBlurEffect(style: .light)) + blurView.frame = view.bounds + blurView.alpha = 0.5 + blurView.addGestureRecognizer( + UITapGestureRecognizer(target: self, + action: #selector(hideDatePicker))) + view.insertSubview(blurView, belowSubview: datePickerModal) + } + } + + @objc private func hideDatePicker() { + let blurView = view.subviews.first() { $0 is UIVisualEffectView } + blurView?.removeFromSuperview() + datePickerModal.isHidden = true + } + @objc private func selectDateInDatePicker(_ sender: UIDatePicker) { + deliverDate(sender.date) + hideDatePicker() + } + + @objc private func deliverDate(_ dateToDeliver: Date) { + currentCalendarVC.yearAndMonth = (dateToDeliver.year * 100) + dateToDeliver.month + prevCalendarVC.yearAndMonth = (dateToDeliver.aMonthAgo.year * 100) + dateToDeliver.aMonthAgo.month + nextCalendarVC.yearAndMonth = (dateToDeliver.aMonthAfter.year * 100) + dateToDeliver.aMonthAfter.month + updateLabel(with: dateToDeliver) + } + + private func updateLabel(with date: Date) { + let formatter = DateFormatter() + formatter.dateFormat = "MMM YYYY" + monthLabel.text = formatter.string(from: date) + } + + // MARK:- Init + + private let today = Date() + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + overrideUserInterfaceStyle = .light + scrollCalendarView.delegate = self + updateLabel(with: today) + initDatePicker() + initNavgationBar() + if modelController.notificationContoller.authorizationStatus == .notDetermined { + modelController.notificationContoller.requestPermission() + } + } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(false) + scrollCalendarView.scrollToChild(currentCalendarView) + } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(false) + deliverDate(Date()) + } + // MARK: Date picker + + private func initDatePicker() { + datePickerModal.layer.borderWidth = 2 + datePickerModal.layer.borderColor = CGColor.salmon + datePickerModal.layer.cornerRadius = 10 + datePicker.datePickerMode = .date + datePicker.preferredDatePickerStyle = .inline + datePickerModal.addSubview(datePicker) + datePicker.addTarget(self, action: #selector(selectDateInDatePicker(_:)), for: .valueChanged) + } + // MARK: Navigation controller + + private func initNavgationBar() { + let newBackButton = UIBarButtonItem(title: "Back", + style: .plain, + target: self, action: #selector(tapBackButton)) + navigationItem.leftBarButtonItem = newBackButton + searchController.loadViewIfNeeded() + searchController.searchResultsUpdater = self + searchController.obscuresBackgroundDuringPresentation = false + searchController.searchBar.enablesReturnKeyAutomatically = false + searchController.searchBar.returnKeyType = .search + definesPresentationContext = true + searchController.searchBar.scopeButtonTitles = ["All"] + Color.Button.allCases.map { $0.rawValue } + searchController.searchBar.delegate = self + } + @objc private func tapBackButton() { + navigationController?.popViewController(animated: true) + } + // MARK:- Search controller delegate + + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + navigationItem.searchController!.isEditing = true + } + + func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + navigationItem.searchController!.isEditing = false + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + navigationItem.searchController!.isEditing = false + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + navigationItem.searchController!.isEditing = false + } +} diff --git a/Schedule_B/ScheduleCollectionVC.swift b/Schedule_B/ScheduleCollectionVC.swift new file mode 100644 index 0000000..431fe22 --- /dev/null +++ b/Schedule_B/ScheduleCollectionVC.swift @@ -0,0 +1,41 @@ +// +// ScheduleViewController.swift +// Schedule_B +// +// Created by KEEN on 2021/02/09. +// + +import UIKit + +// MARK: View Controller draw UI collection view for schedule +protocol ScheduleCollectionVC: UIViewController & UICollectionViewDelegate & UICollectionViewDelegateFlowLayout & UICollectionViewDataSource { + var modelController: ScheduleModelController! { get set } + /// Fill int value from date for each cell ( 2021/02/14 -> 20210214) nil will draw empty cell + var squaresInCalendarView: [Int?] { get set } + +} +extension ScheduleCollectionVC { + func modifyCalendarCell(_ cell: CalendarCellVC, at indexPath: IndexPath, calendarView: UICollectionView) -> CalendarCellVC { + + let dateInt = squaresInCalendarView[indexPath.item] + if dateInt != nil { + // toss data to cell + cell.cellView.date = dateInt + cell.cellView.isToday = dateInt! == Date().toInt + cell.cellView.holiday = modelController.holidayTable[dateInt!] + cell.cellView.schedules = modelController.getSchedules(for: dateInt!) + // Add cell content with Swift UI + cell.hostingController.view.translatesAutoresizingMaskIntoConstraints = false + cell.hostingController.view.frame = cell.contentView.frame + cell.contentView.addSubview(cell.hostingController.view) + }else { + cell.hostingController.rootView.date = nil + cell.hostingController.view.removeFromSuperview() + } + return cell + } + func isEmptyCell(_ cell: CalendarCellVC) -> Bool { + return cell.cellView.date == nil + } +} + diff --git a/Schedule_B/ScheduleModelController.swift b/Schedule_B/ScheduleModelController.swift new file mode 100644 index 0000000..da3d422 --- /dev/null +++ b/Schedule_B/ScheduleModelController.swift @@ -0,0 +1,325 @@ +// +// ScheduleModelController.swift +// Schedule_B +// +// Created by KEEN on 2021/02/23. +// + +import Foundation +import Combine + +class ScheduleModelController: ObservableObject { + + /** + Data intergrity + - Warning: Do not modify directly + + - TODO: Change array to set ? + */ + @Published private var schedules: [Schedule] + typealias Table = [Int: [Schedule]] + @Published private(set) var scheduleTable: Table + @Published private(set) var cycleTable: CycleTable + private(set) var holidayTable: [Int: HolidayGather.Holiday] + + private(set) var userSetting = Setting(country: .korea) + lazy var notificationContoller = NotificationController() + + // MARK:- CRUD + + func getSchedules(for dateInt: Int) -> [Schedule] { + var schedulesForDay = [Schedule]() + if let atDate = scheduleTable[dateInt]{ + schedulesForDay += atDate + } + let date = dateInt.toDate! + if let weekdayCycle = cycleTable.weekday[date.weekDay] { + schedulesForDay += weekdayCycle.filter() { + switch $0.time { + case .cycle(since: let startDate, _, _): + return date > startDate + default : + return false + } + } + } + if let dayCycle = cycleTable.day[date.day] { + schedulesForDay += dayCycle.filter(){ + switch $0.time { + case .cycle(since: let startDate, _, _): + return date > startDate + default : + return false + } + } + } + return schedulesForDay + } + /// Add new schedule + /// - Note: Check permission in notification controller before add alarm + /// - Returns: Boolean value return false when try to add alarm without permission + func addNewSchedule(_ newSchedule: Schedule) -> Bool{ + if newSchedule.alarm != nil { + if notificationContoller.authorizationStatus == .authorized { + notificationContoller.setAlarm(of: newSchedule) + }else { + return false + } + } + schedules.append(newSchedule) + enrollToTable(newSchedule) + return true + } + func deleteSchedule(_ scheduleToDelete: Schedule) { + guard let indexToDelete = schedules.firstIndex(of: scheduleToDelete )else { + fatalError("Delete fail schedule not exist:\n \(scheduleToDelete)") + } + schedules.remove(at: indexToDelete) + scheduleTable.delete(scheduleToDelete) + if scheduleToDelete.alarm != nil { + notificationContoller.removeAlarm(of: scheduleToDelete) + } + } + func replaceSchedule(_ oldSchedule: Schedule, to newSchedule: Schedule) -> Bool { + deleteSchedule(oldSchedule) + return addNewSchedule(newSchedule) + } + private func enrollToTable(_ schedule: Schedule) { + switch schedule.time { + case .spot(let date): + let key = date.toInt + scheduleTable.append(schedule, to: key) + case .period(start: let startDate, end: let endDate): + let startKey = startDate.toInt + let endKey = endDate.toInt + let range = startKey...endKey + for key in range { + scheduleTable.append(schedule, to: key) + } + case .cycle(_, for: let factor, every: let values): + switch factor { + case .day: + for day in values { + cycleTable.day.append(schedule, to: day) + } + case .weekday: + for weekday in values { + cycleTable.weekday.append(schedule, to: weekday) + } + } + } + } + + // MARK:- Save & Load + private let userdataFileName = "user_schedule_data" + private var autoSaveCancellable: AnyCancellable? + + func retrieveUserData() { + if let fileData = loadFile(filename: userdataFileName, as: [Schedule].self) { + schedules = fileData + schedules.forEach() { enrollToTable($0) } + } + autoSaveCancellable = $schedules.sink(receiveValue: { [weak weakSelf = self]_ in + let encoder = JSONEncoder() + if let data = try? encoder.encode(weakSelf?.schedules){ + weakSelf?.saveFile(data: data, filename: weakSelf?.userdataFileName ?? "user_schedule_data") + } + }) + } + func checkHolidayData(for year: Int, about country: HolidayGather.CountryCode) { + let fileName = "holiday_data" + "_\(country.rawValue)" + "(\(year))" + + if checkFileExist(for: fileName) { + let JSONData = loadFile( + filename: fileName, + as: HolidayGather.Response.self) + if JSONData == nil, JSONData!.meta["code"] != 200 { + print("Holiday data from server is not valid") + return + } + let holidayData = JSONData!.response.holidays + holidayData.forEach() { + let holiday = HolidayGather.Holiday(from: $0) + holidayTable[holiday.dateInt] = holiday + } + }else { + let holidayGather = HolidayGather() + holidayGather.retrieveHolidays( + about: year, of: country){ [weak weakSelf = self] data in + weakSelf?.saveFile(data: data, filename: fileName) + weakSelf?.checkHolidayData(for: year, + about: country) + } + } + } + + private func saveFile(data: Data, filename: String) { + if let fileURL = getFilePath(for: filename) { + do { + try data.write(to: fileURL, options: .atomic) + }catch { + fatalError("Couln't write json data to \(filename):\n \(error)") + } + }else { + fatalError("Couln't get file path for saving: \(filename)") + } + } + + private func loadFile(filename: String, as returnType: T.Type) -> T? { + let data: Data + let fileURL: URL + if checkFileExist(for: filename) { + fileURL = getFilePath(for: filename)! + }else { + return nil + } + do { + data = try Data(contentsOf: fileURL) + }catch { + fatalError("Couln't load \(filename) from \(fileURL.path): \n \(error)") + } + do { + let decoder = JSONDecoder() + return try decoder.decode(T.self, from: data) + } catch { + fatalError("Couln't parse \(filename): \n\(error)") + } + } + private func getFilePath(for fileName: String) -> URL? { + let fileManager = FileManager.default + guard let documentURL = fileManager.urls(for: .documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first else { + return nil + } + return documentURL.appendingPathComponent(fileName).appendingPathExtension("json") + } + private func checkFileExist(for fileName: String) -> Bool { + if let fileURL = getFilePath(for: fileName) { + do { + return try fileURL.checkResourceIsReachable() + }catch { + return false + } + }else { + return false + } + } + /// Remove all schedule, saved date will be deleted + func removeAllSchedule() -> Bool{ + schedules.removeAll() + scheduleTable.removeAll() + cycleTable.day.removeAll() + cycleTable.weekday.removeAll() + let encoder = JSONEncoder() + if let data = try? encoder.encode(schedules){ + saveFile(data: data, filename: userdataFileName) + return true + }else { + return false + } + } + + struct CycleTable { + fileprivate(set) var weekday: Table + fileprivate(set) var day: Table + } + struct Setting { + var country: HolidayGather.CountryCode + } + + init() { + schedules = [] + scheduleTable = Table() + cycleTable = CycleTable(weekday: Table(), day: Table()) + holidayTable = [Int: HolidayGather.Holiday]() + /* + TEST Dummy data + + */ + // let title = "title" + // let description = "dummy" + // let date1 = DateComponents(calendar: Calendar.current, + // year: 2021, + // month: 2, + // day: 10).date! + // let test1 = Schedule(title: "day", description: description, priority: 1, time: .cycle( + // since: date1, + // for: .day, + // every: [1, 10]), + // alarm: nil) + // let date2 = DateComponents(calendar: Calendar.current, + // year: 2021, + // month: 2, + // day: 10, + // hour: 10).date! + // let test2 = Schedule(title: "weekday", description: description, priority: 2, time: .cycle( + // since: date2, + // for: .weekday, + // every: [1]), + // alarm: nil) + // let date3_start = DateComponents(calendar: Calendar.current, + // year: 2021, + // month: 2, + // day: 1).date! + // let date3_end = DateComponents(calendar: Calendar.current, + // year: 2021, + // month: 2, + // day: 11).date! + // let test3 = Schedule(title: title, description: description, priority: 3, time: .period(start: date3_start, end: date3_end), alarm: nil) + // let date4_start = DateComponents(calendar: Calendar.current, + // year: 2021, + // month: 2, + // day: 10).date! + // let date4_end = DateComponents(calendar: Calendar.current, + // year: 2021, + // month: 2, + // day: 15).date! + // let test4 = Schedule(title: title, description: description, priority: 2, time: .period(start: date4_start, end: date4_end), alarm: nil) + // + // + // schedules = [test1, test2, test3, test4] + // autoSaveCancellable = $schedules.sink(receiveValue: { [weak weakSelf = self]_ in + // let encoder = JSONEncoder() + // if let data = try? encoder.encode(weakSelf?.schedules){ + // weakSelf?.saveFile(data: data, filename: weakSelf?.userdataFileName ?? "user_schedule_data") + // } + // }) + /* + + */ + } +} + +extension Dictionary where Key == Int, Value == [Schedule] { + mutating func append(_ value: Schedule, to key: Int) { + if self[key] == nil{ + self[key] = [Schedule]() + self[key]?.append(value) + }else { + self[key]?.append(value) + } + } + mutating func delete(_ scheduleToDelete: Schedule) { + switch scheduleToDelete.time { + case .spot(let date): + let key = date.toInt + guard let indexInTable = self[key]?.firstIndex(of: scheduleToDelete) else { + fatalError("Delete fail schedule is not in table \n \(scheduleToDelete)") } + self[key]?.remove(at: indexInTable) + case .period(start: let startDate, end: let endDate): + let startKey = startDate.toInt + let endKey = endDate.toInt + let range = startKey...endKey + for key in range { + guard let indexInTable = self[key]?.firstIndex(of: scheduleToDelete) else { + fatalError("Delete fail schedule is not in table \n \(scheduleToDelete)") } + self[key]?.remove(at: indexInTable) + } + case .cycle(_, _, every: let values): + for key in values { + guard let indexInTable = self[key]?.firstIndex(of: scheduleToDelete) else { + fatalError("Delete fail schedule is not in table \n \(scheduleToDelete)") } + self[key]?.remove(at: indexInTable) + } + } + } +} diff --git a/Schedule_B/ScheduleViewController.swift b/Schedule_B/ScheduleViewController.swift deleted file mode 100644 index 708419f..0000000 --- a/Schedule_B/ScheduleViewController.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ScheduleViewController.swift -// Schedule_B -// -// Created by KEEN on 2021/02/09. -// - -import UIKit -import FSCalendar - -class ScheduleViewController: UIViewController { - - @IBOutlet weak var scheduleCalendarView: FSCalendar! - - override func viewDidLoad() { - super.viewDidLoad() - scheduleCalendarView.backgroundColor = UIColor(red: 252/255, green: 232/255, blue: 239/255, alpha: 1) - scheduleCalendarView.appearance.selectionColor = UIColor(red: 240/255, green: 100/255, blue: 100/255, alpha: 1) - scheduleCalendarView.appearance.todayColor = UIColor(red: 249/255, green: 165/255, blue: 160/255, alpha: 1) - scheduleCalendarView.appearance.headerTitleColor = UIColor(red: 180/255, green: 39/255, blue: 0/255, alpha: 1) - scheduleCalendarView.appearance.weekdayTextColor = UIColor(red: 180/255, green: 39/255, blue: 0/255, alpha: 1) - - scheduleCalendarView.scrollEnabled = true - scheduleCalendarView.scrollDirection = .vertical - - } - -} diff --git a/Schedule_B/ScrollViewExtension.swift b/Schedule_B/ScrollViewExtension.swift new file mode 100644 index 0000000..0435b71 --- /dev/null +++ b/Schedule_B/ScrollViewExtension.swift @@ -0,0 +1,18 @@ +// +// ScrollViewExtension.swift +// Schedule_B +// +// Created by KEEN on 2021/02/23. +// + +import UIKit + +extension UIScrollView { + func scrollToChild(_ view:UIView, animated: Bool = false) { + if let origin = view.superview { + let childStartPoint = origin.convert(view.frame.origin, to: self) + self.setContentOffset(CGPoint(x: childStartPoint.x, y: 0), + animated: animated) + } + } +} diff --git a/Schedule_B/SettingViewController.swift b/Schedule_B/SettingViewController.swift new file mode 100644 index 0000000..b5a2c99 --- /dev/null +++ b/Schedule_B/SettingViewController.swift @@ -0,0 +1,60 @@ +// +// SettingViewController.swift +// Schedule_B +// +// Created by Shin on 2/17/21. +// + +import UIKit +import AuthenticationServices + +class SettingViewController: UITableViewController { + // MARK:- Controller + var scheduleModelController: ScheduleModelController! + + // MARK:- Properties + @IBOutlet weak var deleteScheduleDataCell: UITableViewCell! + @IBOutlet weak var connectGoogleCalendarCell: UITableViewCell! + + // MARK:- User intents + + @objc private func tapConnectGoogleCalendar() { + performSegue(withIdentifier: "ConnectGoogleCalendarSegue", sender: nil) + } + + @objc private func tapDeleteScheduleDataCell() { + let alertController = UIAlertController( + title: "스케쥴 삭제", + message: "모든 스케쥴이 삭제됩니다", + preferredStyle: .alert) + alertController.addAction( + UIAlertAction(title: "취소", style: .cancel)) + alertController.addAction(UIAlertAction( + title: "지우기", + style: .destructive) {_ in + if !self.scheduleModelController.removeAllSchedule() { + // Fail to remove + let alertController = UIAlertController(title: "삭제 실패", + message: "유저 데이터를 지우는데 실패하였습니다", + preferredStyle: .alert) + alertController.addAction( + UIAlertAction(title: "확인", + style: .cancel)) + self.present(alertController, animated: true) + } + }) + present(alertController, animated: true) + } + // MARK:- init + + override func viewDidLoad() { + super.viewDidLoad() + if scheduleModelController == nil { + preconditionFailure("Schedule model controller is not connected") + } + tableView.allowsSelection = false + deleteScheduleDataCell.addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(tapDeleteScheduleDataCell))) + connectGoogleCalendarCell.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapConnectGoogleCalendar))) + } +} diff --git a/Schedule_B/SingleCalendarVC.swift b/Schedule_B/SingleCalendarVC.swift new file mode 100644 index 0000000..42ec026 --- /dev/null +++ b/Schedule_B/SingleCalendarVC.swift @@ -0,0 +1,112 @@ +// +// SingleCalendarVC.swift +// Schedule_B +// +// Created by KEEN on 2021/02/23. +// + +import UIKit +import Combine + +class SingleCalendarViewController: UIViewController, ScheduleCollectionVC { + // MARK: Controller + + var modelController: ScheduleModelController! + + // MARK:- Properties + var calendarView: UICollectionView! + private var observeDateTableCancellable: AnyCancellable? + private var observeCycleTableCancellable: AnyCancellable? + private var showDailyViewSegue: (String, Any) -> Void? + private let today = Date() + + var yearAndMonth: Int{ + didSet{ + updateCalendar() + } + } + + var searchRequest = SearchRequest(priority: nil, text: nil) { + didSet { + calendarView.reloadData() + } + } + + struct SearchRequest { + var priority: Int? + var text: String? + } + var squaresInCalendarView = [Int?]() + + private func updateCalendar() { + guard yearAndMonth > 190001 && yearAndMonth < 20500101 else { + fatalError("Attemp to update calendar with invaild year") + } + squaresInCalendarView.removeAll() + let totalDays = Calendar.getDaysInMonth(yearAndMonth) + let firstDay = Calendar.firstDateOfMonth(yearAndMonth) + let startSqureNumber = firstDay.weekDay - 1 + + for index in 0...41 { + if index < startSqureNumber || (index - startSqureNumber + 1) > totalDays{ + squaresInCalendarView.append(nil) + }else { + let date = index - startSqureNumber + 1 + squaresInCalendarView.append((yearAndMonth * 100) + date) + } + } + calendarView.reloadData() + } + + // MARK:- Collection view delegate + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + squaresInCalendarView.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + var cell = calendarView.dequeueReusableCell( + withReuseIdentifier: "CalendarCell", + for: indexPath) as! CalendarCellVC + cell = modifyCalendarCell(cell, at: indexPath, calendarView: calendarView) + if !isEmptyCell(cell) { + cell.cellView.searchRequest = searchRequest + } + return cell + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CalendarCellVC.size(in: (calendarView.frame.size)) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if let dateToShow = squaresInCalendarView[indexPath.row]{ + showDailyViewSegue("ShowDailyView", dateToShow) + } + } + + // MARK:- init + + init(of calendarView: UICollectionView, on yearAndMonth: Int, modelController: ScheduleModelController, segue: @escaping (String, Any) -> Void = { _,_ in}) { + self.calendarView = calendarView + self.yearAndMonth = yearAndMonth + self.modelController = modelController + self.showDailyViewSegue = segue + super.init(nibName: nil, bundle: nil) + self.observeDateTableCancellable = modelController.$scheduleTable.sink() { [weak weakSelf = self] table in + weakSelf?.updateCalendar() + } + self.observeCycleTableCancellable = + modelController.$cycleTable.sink() { [weak weakSelf = self] table in + weakSelf?.updateCalendar() + } + + calendarView.isScrollEnabled = false + calendarView.dataSource = self + calendarView.delegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Schedule_B/UIViewExtension.swift b/Schedule_B/UIViewExtension.swift new file mode 100644 index 0000000..dc692e5 --- /dev/null +++ b/Schedule_B/UIViewExtension.swift @@ -0,0 +1,35 @@ +// +// UIViewExtension.swift +// Schedule_B +// +// Created by KEEN on 2021/02/23. +// + +import UIKit + +extension UIView { + + var cornerRadius: CGFloat { + get { + return layer.cornerRadius + } + set { + layer.cornerRadius = newValue + } + } + var borderColor: UIColor? { + get { + if let color = layer.borderColor { + return UIColor(cgColor: color) + } + return nil + } + set { + if let color = newValue { + layer.borderColor = color.cgColor + } else { + layer.borderColor = nil + } + } + } +} diff --git a/Schedule_B/URLRequestExtension.swift b/Schedule_B/URLRequestExtension.swift new file mode 100644 index 0000000..a2d589c --- /dev/null +++ b/Schedule_B/URLRequestExtension.swift @@ -0,0 +1,29 @@ +// +// Promise.swift +// Schedule_B +// +// Created by Shin on 2/20/21. +// + +import Foundation + +extension URLRequest { + func sendWithPromise(_ givenPromise: Promise? = nil) -> Future { + let promise = givenPromise == nil ? Promise(): givenPromise! + + let task = URLSession.shared.dataTask(with: self) { + data, response, error in + print("Reponse of data task") + print(response!) + if let error = error { + promise.reject(with: error) + }else { + promise.resolve(with: data ?? Data()) + } + } + task.resume() + + return promise + } +} + diff --git a/Schedule_B/ViewControllerExtension.swift b/Schedule_B/ViewControllerExtension.swift new file mode 100644 index 0000000..3b85b1e --- /dev/null +++ b/Schedule_B/ViewControllerExtension.swift @@ -0,0 +1,15 @@ +// +// ViewControllerExtension.swift +// Schedule_B +// +// Created by Shin on 2/20/21. +// + +import UIKit +import AuthenticationServices + +extension UIViewController: ASWebAuthenticationPresentationContextProviding { + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return view.window! + } +}