├── .gitignore ├── ElegantCalendar.xcassets ├── Contents.json ├── brilliantViolet.colorset │ └── Contents.json ├── craftBrown.colorset │ └── Contents.json ├── fluorescentPink.colorset │ └── Contents.json ├── kiwiGreen.colorset │ └── Contents.json ├── mauvePurple.colorset │ └── Contents.json ├── orangeYellow.colorset │ └── Contents.json ├── red.colorset │ └── Contents.json ├── royalBlue.colorset │ └── Contents.json └── uturn.left.imageset │ ├── Contents.json │ └── uturn.left.png ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Example.xcscheme └── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── blackPearl.colorset │ │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Calendar with Accessory View │ ├── ExampleCalendarView.swift │ ├── Visit.swift │ ├── VisitCell.swift │ ├── VisitPreviewConstants.swift │ └── VisitsListView.swift │ ├── ContentView.swift │ ├── Individual Views │ ├── ExampleMonthlyCalendarView.swift │ └── ExampleYearlyCalendarView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── Selection and Exit │ └── ExampleSelectionView.swift │ └── Shared │ ├── ChangeThemeButton.swift │ ├── Color+Custom.swift │ ├── Date+Additions.swift │ └── LightDarkThemePreview.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Screenshots ├── brilliantViolet.PNG ├── craftBrown.PNG ├── fluorescentPink.PNG ├── kiwiGreen.PNG ├── mauvePurple.PNG ├── orangeYellow.PNG ├── red.PNG └── royalblue.PNG ├── Sources └── ElegantCalendar │ ├── Helpers │ ├── Extensions │ │ ├── Axis+Invert.swift │ │ ├── Calender+Dates.swift │ │ ├── Color+CustomColors.swift │ │ ├── Date+DaysFromToday.swift │ │ ├── Date+toString.swift │ │ ├── Enumeration+Matching.swift │ │ ├── EnvironmentKey+CalendarTheme.swift │ │ ├── Image+Custom.swift │ │ ├── UIImage+BundleInit.swift │ │ └── UIImpactFeedbackGenerator+Haptic.swift │ ├── Models │ │ ├── CalendarConfiguration.swift │ │ ├── CalenderConstants.swift │ │ ├── ObservableObjects │ │ │ ├── ElegantCalendarManager.swift │ │ │ ├── MonthlyCalendarManager.swift │ │ │ ├── PagerState.swift │ │ │ └── YearlyCalendarManager.swift │ │ └── Protocols │ │ │ ├── Calendar+Axis.swift │ │ │ ├── Calendar+Buildable.swift │ │ │ ├── ElegantCalendarCommunicator.swift │ │ │ ├── ElegantCalendarDataSource.swift │ │ │ └── ElegantCalendarDelegate.swift │ └── Previews │ │ └── LightDarkThemePreview.swift │ └── Views │ ├── ElegantCalendarView.swift │ ├── Monthly │ ├── DayView.swift │ ├── MonthView.swift │ ├── MonthlyCalendarView.swift │ └── WeekView.swift │ ├── Shared │ └── ScrollBackToTodayButton.swift │ └── Yearly │ ├── SmallDayView.swift │ ├── SmallMonthView.swift │ ├── SmallWeekView.swift │ ├── YearView.swift │ └── YearlyCalendarView.swift └── resources.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/brilliantViolet.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.573", 9 | "green" : "0.227", 10 | "red" : "0.271" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/craftBrown.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.388", 9 | "green" : "0.533", 10 | "red" : "0.659" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/fluorescentPink.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.302", 9 | "green" : "0.086", 10 | "red" : "0.725" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/kiwiGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.161", 9 | "green" : "0.557", 10 | "red" : "0.459" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/mauvePurple.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.451", 9 | "green" : "0.165", 10 | "red" : "0.580" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/orangeYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.161", 9 | "green" : "0.529", 10 | "red" : "0.859" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.110", 9 | "green" : "0.125", 10 | "red" : "0.694" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/royalBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.584", 9 | "green" : "0.325", 10 | "red" : "0.094" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/uturn.left.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "uturn.left.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ElegantCalendar.xcassets/uturn.left.imageset/uturn.left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThasianX/ElegantCalendar/3d1e8369ffb233e392ee36ad6ff0fd38ad4846b2/ElegantCalendar.xcassets/uturn.left.imageset/uturn.left.png -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1E029C7724A4448A00A81FFD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E029C7624A4448A00A81FFD /* AppDelegate.swift */; }; 11 | 1E029C7924A4448A00A81FFD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E029C7824A4448A00A81FFD /* SceneDelegate.swift */; }; 12 | 1E029C7D24A4448A00A81FFD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1E029C7C24A4448A00A81FFD /* Assets.xcassets */; }; 13 | 1E029C8024A4448A00A81FFD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1E029C7F24A4448A00A81FFD /* Preview Assets.xcassets */; }; 14 | 1E029C8324A4448A00A81FFD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1E029C8124A4448A00A81FFD /* LaunchScreen.storyboard */; }; 15 | 1E029CB324A4450200A81FFD /* ExampleCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E029CB024A4450200A81FFD /* ExampleCalendarView.swift */; }; 16 | 1E029CB424A4450200A81FFD /* ExampleMonthlyCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E029CB124A4450200A81FFD /* ExampleMonthlyCalendarView.swift */; }; 17 | 1E029CB524A4450200A81FFD /* ExampleYearlyCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E029CB224A4450200A81FFD /* ExampleYearlyCalendarView.swift */; }; 18 | 1E161AF724D5F46200CA2B7F /* Date+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E161AF624D5F46200CA2B7F /* Date+Additions.swift */; }; 19 | 1E161AFA24D5F47D00CA2B7F /* ElegantCalendar in Frameworks */ = {isa = PBXBuildFile; productRef = 1E161AF924D5F47D00CA2B7F /* ElegantCalendar */; }; 20 | 1E161AFC24D5F51000CA2B7F /* LightDarkThemePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E161AFB24D5F51000CA2B7F /* LightDarkThemePreview.swift */; }; 21 | 1E161AFE24D5F59500CA2B7F /* ElegantCalendar.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1E161AFD24D5F59500CA2B7F /* ElegantCalendar.xcassets */; }; 22 | 1E628BED24A451B900DDD18E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E628BEC24A451B900DDD18E /* ContentView.swift */; }; 23 | 1E628BEF24A4528D00DDD18E /* ExampleSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E628BEE24A4528D00DDD18E /* ExampleSelectionView.swift */; }; 24 | 1E995AF424A552BA00F436BE /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E995AF024A552BA00F436BE /* Visit.swift */; }; 25 | 1E995AF524A552BA00F436BE /* VisitsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E995AF124A552BA00F436BE /* VisitsListView.swift */; }; 26 | 1E995AF624A552BA00F436BE /* VisitPreviewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E995AF224A552BA00F436BE /* VisitPreviewConstants.swift */; }; 27 | 1E995AF724A552BA00F436BE /* VisitCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E995AF324A552BA00F436BE /* VisitCell.swift */; }; 28 | 1EE7C01B24BEA4B3000573A2 /* ChangeThemeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE7C01A24BEA4B3000573A2 /* ChangeThemeButton.swift */; }; 29 | 1EE7C01D24BEA55B000573A2 /* Color+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE7C01C24BEA55B000573A2 /* Color+Custom.swift */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 1E029C7324A4448A00A81FFD /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 1E029C7624A4448A00A81FFD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 35 | 1E029C7824A4448A00A81FFD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 36 | 1E029C7C24A4448A00A81FFD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | 1E029C7F24A4448A00A81FFD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 38 | 1E029C8224A4448A00A81FFD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 39 | 1E029C8424A4448A00A81FFD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 1E029CB024A4450200A81FFD /* ExampleCalendarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleCalendarView.swift; sourceTree = ""; }; 41 | 1E029CB124A4450200A81FFD /* ExampleMonthlyCalendarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleMonthlyCalendarView.swift; sourceTree = ""; }; 42 | 1E029CB224A4450200A81FFD /* ExampleYearlyCalendarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleYearlyCalendarView.swift; sourceTree = ""; }; 43 | 1E161AF524D5F43C00CA2B7F /* ElegantCalendar */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ElegantCalendar; path = ../..; sourceTree = ""; }; 44 | 1E161AF624D5F46200CA2B7F /* Date+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Additions.swift"; sourceTree = ""; }; 45 | 1E161AFB24D5F51000CA2B7F /* LightDarkThemePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightDarkThemePreview.swift; sourceTree = ""; }; 46 | 1E161AFD24D5F59500CA2B7F /* ElegantCalendar.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = ElegantCalendar.xcassets; path = ../ElegantCalendar.xcassets; sourceTree = ""; }; 47 | 1E628BEC24A451B900DDD18E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 48 | 1E628BEE24A4528D00DDD18E /* ExampleSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleSelectionView.swift; sourceTree = ""; }; 49 | 1E995AF024A552BA00F436BE /* Visit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Visit.swift; sourceTree = ""; }; 50 | 1E995AF124A552BA00F436BE /* VisitsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisitsListView.swift; sourceTree = ""; }; 51 | 1E995AF224A552BA00F436BE /* VisitPreviewConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisitPreviewConstants.swift; sourceTree = ""; }; 52 | 1E995AF324A552BA00F436BE /* VisitCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisitCell.swift; sourceTree = ""; }; 53 | 1EE7C01A24BEA4B3000573A2 /* ChangeThemeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeThemeButton.swift; sourceTree = ""; }; 54 | 1EE7C01C24BEA55B000573A2 /* Color+Custom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Custom.swift"; sourceTree = ""; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | 1E029C7024A4448A00A81FFD /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | 1E161AFA24D5F47D00CA2B7F /* ElegantCalendar in Frameworks */, 63 | ); 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | /* End PBXFrameworksBuildPhase section */ 67 | 68 | /* Begin PBXGroup section */ 69 | 1E029C6A24A4448A00A81FFD = { 70 | isa = PBXGroup; 71 | children = ( 72 | 1E161AFD24D5F59500CA2B7F /* ElegantCalendar.xcassets */, 73 | 1E029C7524A4448A00A81FFD /* Example */, 74 | 1E029C7424A4448A00A81FFD /* Products */, 75 | 1E161AF824D5F47D00CA2B7F /* Frameworks */, 76 | ); 77 | sourceTree = ""; 78 | }; 79 | 1E029C7424A4448A00A81FFD /* Products */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 1E029C7324A4448A00A81FFD /* Example.app */, 83 | ); 84 | name = Products; 85 | sourceTree = ""; 86 | }; 87 | 1E029C7524A4448A00A81FFD /* Example */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 1E029C7624A4448A00A81FFD /* AppDelegate.swift */, 91 | 1E029C7824A4448A00A81FFD /* SceneDelegate.swift */, 92 | 1E628BEC24A451B900DDD18E /* ContentView.swift */, 93 | 1E029D3624A44CBC00A81FFD /* Individual Views */, 94 | 1E029D3724A44CF700A81FFD /* Calendar with Accessory View */, 95 | 1E029D3524A44CA700A81FFD /* Selection and Exit */, 96 | 1E029D3824A44D2D00A81FFD /* Shared */, 97 | 1E029C7C24A4448A00A81FFD /* Assets.xcassets */, 98 | 1E029C8124A4448A00A81FFD /* LaunchScreen.storyboard */, 99 | 1E029C8424A4448A00A81FFD /* Info.plist */, 100 | 1E161AF524D5F43C00CA2B7F /* ElegantCalendar */, 101 | 1E029C7E24A4448A00A81FFD /* Preview Content */, 102 | ); 103 | path = Example; 104 | sourceTree = ""; 105 | }; 106 | 1E029C7E24A4448A00A81FFD /* Preview Content */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 1E029C7F24A4448A00A81FFD /* Preview Assets.xcassets */, 110 | ); 111 | path = "Preview Content"; 112 | sourceTree = ""; 113 | }; 114 | 1E029D3524A44CA700A81FFD /* Selection and Exit */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 1E628BEE24A4528D00DDD18E /* ExampleSelectionView.swift */, 118 | ); 119 | path = "Selection and Exit"; 120 | sourceTree = ""; 121 | }; 122 | 1E029D3624A44CBC00A81FFD /* Individual Views */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 1E029CB124A4450200A81FFD /* ExampleMonthlyCalendarView.swift */, 126 | 1E029CB224A4450200A81FFD /* ExampleYearlyCalendarView.swift */, 127 | ); 128 | path = "Individual Views"; 129 | sourceTree = ""; 130 | }; 131 | 1E029D3724A44CF700A81FFD /* Calendar with Accessory View */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 1E029CB024A4450200A81FFD /* ExampleCalendarView.swift */, 135 | 1E995AF024A552BA00F436BE /* Visit.swift */, 136 | 1E995AF324A552BA00F436BE /* VisitCell.swift */, 137 | 1E995AF224A552BA00F436BE /* VisitPreviewConstants.swift */, 138 | 1E995AF124A552BA00F436BE /* VisitsListView.swift */, 139 | ); 140 | path = "Calendar with Accessory View"; 141 | sourceTree = ""; 142 | }; 143 | 1E029D3824A44D2D00A81FFD /* Shared */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 1EE7C01A24BEA4B3000573A2 /* ChangeThemeButton.swift */, 147 | 1EE7C01C24BEA55B000573A2 /* Color+Custom.swift */, 148 | 1E161AF624D5F46200CA2B7F /* Date+Additions.swift */, 149 | 1E161AFB24D5F51000CA2B7F /* LightDarkThemePreview.swift */, 150 | ); 151 | path = Shared; 152 | sourceTree = ""; 153 | }; 154 | 1E161AF824D5F47D00CA2B7F /* Frameworks */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | ); 158 | name = Frameworks; 159 | sourceTree = ""; 160 | }; 161 | /* End PBXGroup section */ 162 | 163 | /* Begin PBXNativeTarget section */ 164 | 1E029C7224A4448A00A81FFD /* Example */ = { 165 | isa = PBXNativeTarget; 166 | buildConfigurationList = 1E029C8724A4448A00A81FFD /* Build configuration list for PBXNativeTarget "Example" */; 167 | buildPhases = ( 168 | 1E029C6F24A4448A00A81FFD /* Sources */, 169 | 1E029C7024A4448A00A81FFD /* Frameworks */, 170 | 1E029C7124A4448A00A81FFD /* Resources */, 171 | ); 172 | buildRules = ( 173 | ); 174 | dependencies = ( 175 | ); 176 | name = Example; 177 | packageProductDependencies = ( 178 | 1E161AF924D5F47D00CA2B7F /* ElegantCalendar */, 179 | ); 180 | productName = Example; 181 | productReference = 1E029C7324A4448A00A81FFD /* Example.app */; 182 | productType = "com.apple.product-type.application"; 183 | }; 184 | /* End PBXNativeTarget section */ 185 | 186 | /* Begin PBXProject section */ 187 | 1E029C6B24A4448A00A81FFD /* Project object */ = { 188 | isa = PBXProject; 189 | attributes = { 190 | LastSwiftUpdateCheck = 1150; 191 | LastUpgradeCheck = 1150; 192 | ORGANIZATIONNAME = "Kevin Li"; 193 | TargetAttributes = { 194 | 1E029C7224A4448A00A81FFD = { 195 | CreatedOnToolsVersion = 11.5; 196 | }; 197 | }; 198 | }; 199 | buildConfigurationList = 1E029C6E24A4448A00A81FFD /* Build configuration list for PBXProject "Example" */; 200 | compatibilityVersion = "Xcode 9.3"; 201 | developmentRegion = en; 202 | hasScannedForEncodings = 0; 203 | knownRegions = ( 204 | en, 205 | Base, 206 | ); 207 | mainGroup = 1E029C6A24A4448A00A81FFD; 208 | packageReferences = ( 209 | ); 210 | productRefGroup = 1E029C7424A4448A00A81FFD /* Products */; 211 | projectDirPath = ""; 212 | projectRoot = ""; 213 | targets = ( 214 | 1E029C7224A4448A00A81FFD /* Example */, 215 | ); 216 | }; 217 | /* End PBXProject section */ 218 | 219 | /* Begin PBXResourcesBuildPhase section */ 220 | 1E029C7124A4448A00A81FFD /* Resources */ = { 221 | isa = PBXResourcesBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | 1E029C8324A4448A00A81FFD /* LaunchScreen.storyboard in Resources */, 225 | 1E029C8024A4448A00A81FFD /* Preview Assets.xcassets in Resources */, 226 | 1E029C7D24A4448A00A81FFD /* Assets.xcassets in Resources */, 227 | 1E161AFE24D5F59500CA2B7F /* ElegantCalendar.xcassets in Resources */, 228 | ); 229 | runOnlyForDeploymentPostprocessing = 0; 230 | }; 231 | /* End PBXResourcesBuildPhase section */ 232 | 233 | /* Begin PBXSourcesBuildPhase section */ 234 | 1E029C6F24A4448A00A81FFD /* Sources */ = { 235 | isa = PBXSourcesBuildPhase; 236 | buildActionMask = 2147483647; 237 | files = ( 238 | 1EE7C01D24BEA55B000573A2 /* Color+Custom.swift in Sources */, 239 | 1E029CB424A4450200A81FFD /* ExampleMonthlyCalendarView.swift in Sources */, 240 | 1E995AF424A552BA00F436BE /* Visit.swift in Sources */, 241 | 1E029C7724A4448A00A81FFD /* AppDelegate.swift in Sources */, 242 | 1EE7C01B24BEA4B3000573A2 /* ChangeThemeButton.swift in Sources */, 243 | 1E161AFC24D5F51000CA2B7F /* LightDarkThemePreview.swift in Sources */, 244 | 1E995AF724A552BA00F436BE /* VisitCell.swift in Sources */, 245 | 1E029C7924A4448A00A81FFD /* SceneDelegate.swift in Sources */, 246 | 1E995AF624A552BA00F436BE /* VisitPreviewConstants.swift in Sources */, 247 | 1E628BED24A451B900DDD18E /* ContentView.swift in Sources */, 248 | 1E161AF724D5F46200CA2B7F /* Date+Additions.swift in Sources */, 249 | 1E995AF524A552BA00F436BE /* VisitsListView.swift in Sources */, 250 | 1E628BEF24A4528D00DDD18E /* ExampleSelectionView.swift in Sources */, 251 | 1E029CB324A4450200A81FFD /* ExampleCalendarView.swift in Sources */, 252 | 1E029CB524A4450200A81FFD /* ExampleYearlyCalendarView.swift in Sources */, 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXSourcesBuildPhase section */ 257 | 258 | /* Begin PBXVariantGroup section */ 259 | 1E029C8124A4448A00A81FFD /* LaunchScreen.storyboard */ = { 260 | isa = PBXVariantGroup; 261 | children = ( 262 | 1E029C8224A4448A00A81FFD /* Base */, 263 | ); 264 | name = LaunchScreen.storyboard; 265 | sourceTree = ""; 266 | }; 267 | /* End PBXVariantGroup section */ 268 | 269 | /* Begin XCBuildConfiguration section */ 270 | 1E029C8524A4448A00A81FFD /* Debug */ = { 271 | isa = XCBuildConfiguration; 272 | buildSettings = { 273 | ALWAYS_SEARCH_USER_PATHS = NO; 274 | CLANG_ANALYZER_NONNULL = YES; 275 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 276 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 277 | CLANG_CXX_LIBRARY = "libc++"; 278 | CLANG_ENABLE_MODULES = YES; 279 | CLANG_ENABLE_OBJC_ARC = YES; 280 | CLANG_ENABLE_OBJC_WEAK = YES; 281 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 282 | CLANG_WARN_BOOL_CONVERSION = YES; 283 | CLANG_WARN_COMMA = YES; 284 | CLANG_WARN_CONSTANT_CONVERSION = YES; 285 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 286 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 287 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 288 | CLANG_WARN_EMPTY_BODY = YES; 289 | CLANG_WARN_ENUM_CONVERSION = YES; 290 | CLANG_WARN_INFINITE_RECURSION = YES; 291 | CLANG_WARN_INT_CONVERSION = YES; 292 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 293 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 294 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 295 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 296 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 297 | CLANG_WARN_STRICT_PROTOTYPES = YES; 298 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 299 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 300 | CLANG_WARN_UNREACHABLE_CODE = YES; 301 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 302 | COPY_PHASE_STRIP = NO; 303 | DEBUG_INFORMATION_FORMAT = dwarf; 304 | ENABLE_STRICT_OBJC_MSGSEND = YES; 305 | ENABLE_TESTABILITY = YES; 306 | GCC_C_LANGUAGE_STANDARD = gnu11; 307 | GCC_DYNAMIC_NO_PIC = NO; 308 | GCC_NO_COMMON_BLOCKS = YES; 309 | GCC_OPTIMIZATION_LEVEL = 0; 310 | GCC_PREPROCESSOR_DEFINITIONS = ( 311 | "DEBUG=1", 312 | "$(inherited)", 313 | ); 314 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 315 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 316 | GCC_WARN_UNDECLARED_SELECTOR = YES; 317 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 318 | GCC_WARN_UNUSED_FUNCTION = YES; 319 | GCC_WARN_UNUSED_VARIABLE = YES; 320 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 321 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 322 | MTL_FAST_MATH = YES; 323 | ONLY_ACTIVE_ARCH = YES; 324 | SDKROOT = iphoneos; 325 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 326 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 327 | }; 328 | name = Debug; 329 | }; 330 | 1E029C8624A4448A00A81FFD /* Release */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | ALWAYS_SEARCH_USER_PATHS = NO; 334 | CLANG_ANALYZER_NONNULL = YES; 335 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 336 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 337 | CLANG_CXX_LIBRARY = "libc++"; 338 | CLANG_ENABLE_MODULES = YES; 339 | CLANG_ENABLE_OBJC_ARC = YES; 340 | CLANG_ENABLE_OBJC_WEAK = YES; 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 347 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 348 | CLANG_WARN_EMPTY_BODY = YES; 349 | CLANG_WARN_ENUM_CONVERSION = YES; 350 | CLANG_WARN_INFINITE_RECURSION = YES; 351 | CLANG_WARN_INT_CONVERSION = YES; 352 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 353 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 354 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 355 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 356 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 357 | CLANG_WARN_STRICT_PROTOTYPES = YES; 358 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 359 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 360 | CLANG_WARN_UNREACHABLE_CODE = YES; 361 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 362 | COPY_PHASE_STRIP = NO; 363 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 364 | ENABLE_NS_ASSERTIONS = NO; 365 | ENABLE_STRICT_OBJC_MSGSEND = YES; 366 | GCC_C_LANGUAGE_STANDARD = gnu11; 367 | GCC_NO_COMMON_BLOCKS = YES; 368 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 369 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 370 | GCC_WARN_UNDECLARED_SELECTOR = YES; 371 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 372 | GCC_WARN_UNUSED_FUNCTION = YES; 373 | GCC_WARN_UNUSED_VARIABLE = YES; 374 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 375 | MTL_ENABLE_DEBUG_INFO = NO; 376 | MTL_FAST_MATH = YES; 377 | SDKROOT = iphoneos; 378 | SWIFT_COMPILATION_MODE = wholemodule; 379 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 380 | VALIDATE_PRODUCT = YES; 381 | }; 382 | name = Release; 383 | }; 384 | 1E029C8824A4448A00A81FFD /* Debug */ = { 385 | isa = XCBuildConfiguration; 386 | buildSettings = { 387 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 388 | CODE_SIGN_STYLE = Automatic; 389 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 390 | DEVELOPMENT_TEAM = 75H2NCGMKS; 391 | ENABLE_PREVIEWS = YES; 392 | INFOPLIST_FILE = Example/Info.plist; 393 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 394 | LD_RUNPATH_SEARCH_PATHS = ( 395 | "$(inherited)", 396 | "@executable_path/Frameworks", 397 | ); 398 | PRODUCT_BUNDLE_IDENTIFIER = com.kevinli.Example; 399 | PRODUCT_NAME = "$(TARGET_NAME)"; 400 | SWIFT_VERSION = 5.0; 401 | TARGETED_DEVICE_FAMILY = "1,2"; 402 | }; 403 | name = Debug; 404 | }; 405 | 1E029C8924A4448A00A81FFD /* Release */ = { 406 | isa = XCBuildConfiguration; 407 | buildSettings = { 408 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 409 | CODE_SIGN_STYLE = Automatic; 410 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 411 | DEVELOPMENT_TEAM = 75H2NCGMKS; 412 | ENABLE_PREVIEWS = YES; 413 | INFOPLIST_FILE = Example/Info.plist; 414 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 415 | LD_RUNPATH_SEARCH_PATHS = ( 416 | "$(inherited)", 417 | "@executable_path/Frameworks", 418 | ); 419 | PRODUCT_BUNDLE_IDENTIFIER = com.kevinli.Example; 420 | PRODUCT_NAME = "$(TARGET_NAME)"; 421 | SWIFT_VERSION = 5.0; 422 | TARGETED_DEVICE_FAMILY = "1,2"; 423 | }; 424 | name = Release; 425 | }; 426 | /* End XCBuildConfiguration section */ 427 | 428 | /* Begin XCConfigurationList section */ 429 | 1E029C6E24A4448A00A81FFD /* Build configuration list for PBXProject "Example" */ = { 430 | isa = XCConfigurationList; 431 | buildConfigurations = ( 432 | 1E029C8524A4448A00A81FFD /* Debug */, 433 | 1E029C8624A4448A00A81FFD /* Release */, 434 | ); 435 | defaultConfigurationIsVisible = 0; 436 | defaultConfigurationName = Release; 437 | }; 438 | 1E029C8724A4448A00A81FFD /* Build configuration list for PBXNativeTarget "Example" */ = { 439 | isa = XCConfigurationList; 440 | buildConfigurations = ( 441 | 1E029C8824A4448A00A81FFD /* Debug */, 442 | 1E029C8924A4448A00A81FFD /* Release */, 443 | ); 444 | defaultConfigurationIsVisible = 0; 445 | defaultConfigurationName = Release; 446 | }; 447 | /* End XCConfigurationList section */ 448 | 449 | /* Begin XCSwiftPackageProductDependency section */ 450 | 1E161AF924D5F47D00CA2B7F /* ElegantCalendar */ = { 451 | isa = XCSwiftPackageProductDependency; 452 | productName = ElegantCalendar; 453 | }; 454 | /* End XCSwiftPackageProductDependency section */ 455 | }; 456 | rootObject = 1E029C6B24A4448A00A81FFD /* Project object */; 457 | } 458 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ElegantPages", 6 | "repositoryURL": "https://github.com/ThasianX/ElegantPages", 7 | "state": { 8 | "branch": null, 9 | "revision": "47d166799f2313ad9e47c9c22687021d396d940f", 10 | "version": "1.4.1" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:33 PM - 6/24/20 2 | 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | 8 | 9 | 10 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 11 | // Override point for customization after application launch. 12 | return true 13 | } 14 | 15 | // MARK: UISceneSession Lifecycle 16 | 17 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 18 | // Called when a new scene session is being created. 19 | // Use this method to select a configuration to create the new scene with. 20 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 21 | } 22 | 23 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 24 | // Called when the user discards a scene session. 25 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 26 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 27 | } 28 | 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/blackPearl.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.553", 9 | "green" : "0.216", 10 | "red" : "0.259" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.686", 27 | "green" : "0.486", 28 | "red" : "0.533" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "0.553", 45 | "green" : "0.216", 46 | "red" : "0.259" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Example/Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/Example/Calendar with Accessory View/ExampleCalendarView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:47 AM - 6/13/20 2 | 3 | import ElegantCalendar 4 | import SwiftUI 5 | 6 | struct ExampleCalendarView: View { 7 | 8 | @ObservedObject private var calendarManager: ElegantCalendarManager 9 | 10 | let visitsByDay: [Date: [Visit]] 11 | 12 | @State private var calendarTheme: CalendarTheme = .royalBlue 13 | 14 | init(ascVisits: [Visit], initialMonth: Date?) { 15 | let configuration = CalendarConfiguration( 16 | calendar: currentCalendar, 17 | startDate: ascVisits.first!.arrivalDate, 18 | endDate: ascVisits.last!.arrivalDate) 19 | 20 | calendarManager = ElegantCalendarManager( 21 | configuration: configuration, 22 | initialMonth: initialMonth) 23 | 24 | visitsByDay = Dictionary( 25 | grouping: ascVisits, 26 | by: { currentCalendar.startOfDay(for: $0.arrivalDate) }) 27 | 28 | calendarManager.datasource = self 29 | calendarManager.delegate = self 30 | } 31 | 32 | var body: some View { 33 | ZStack { 34 | ElegantCalendarView(calendarManager: calendarManager) 35 | .theme(calendarTheme) 36 | VStack { 37 | Spacer() 38 | changeThemeButton 39 | .padding(.bottom, 50) 40 | } 41 | } 42 | } 43 | 44 | private var changeThemeButton: some View { 45 | ChangeThemeButton(calendarTheme: $calendarTheme) 46 | } 47 | 48 | } 49 | 50 | extension ExampleCalendarView: ElegantCalendarDataSource { 51 | 52 | func calendar(backgroundColorOpacityForDate date: Date) -> Double { 53 | let startOfDay = currentCalendar.startOfDay(for: date) 54 | return Double((visitsByDay[startOfDay]?.count ?? 0) + 3) / 15.0 55 | } 56 | 57 | func calendar(canSelectDate date: Date) -> Bool { 58 | let day = currentCalendar.dateComponents([.day], from: date).day! 59 | return day != 4 60 | } 61 | 62 | func calendar(viewForSelectedDate date: Date, dimensions size: CGSize) -> AnyView { 63 | let startOfDay = currentCalendar.startOfDay(for: date) 64 | return VisitsListView(visits: visitsByDay[startOfDay] ?? [], height: size.height).erased 65 | } 66 | 67 | } 68 | 69 | extension ExampleCalendarView: ElegantCalendarDelegate { 70 | 71 | func calendar(didSelectDay date: Date) { 72 | print("Selected date: \(date)") 73 | } 74 | 75 | func calendar(willDisplayMonth date: Date) { 76 | print("Month displayed: \(date)") 77 | } 78 | 79 | func calendar(didSelectMonth date: Date) { 80 | print("Selected month: \(date)") 81 | } 82 | 83 | func calendar(willDisplayYear date: Date) { 84 | print("Year displayed: \(date)") 85 | } 86 | 87 | } 88 | 89 | struct ExampleCalendarView_Previews: PreviewProvider { 90 | static var previews: some View { 91 | ExampleCalendarView(ascVisits: Visit.mocks(start: .daysFromToday(-365*2), end: .daysFromToday(365*2)), initialMonth: nil) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Example/Example/Calendar with Accessory View/Visit.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 1:32 PM - 6/13/20 2 | 3 | import SwiftUI 4 | 5 | let currentCalendar = Calendar.current 6 | let screen = UIScreen.main.bounds 7 | 8 | struct Visit { 9 | 10 | let locationName: String 11 | let tagColor: Color 12 | let arrivalDate: Date 13 | let departureDate: Date 14 | 15 | var duration: String { 16 | arrivalDate.timeOnlyWithPadding + " ➝ " + departureDate.timeOnlyWithPadding 17 | } 18 | 19 | } 20 | 21 | extension Visit: Identifiable { 22 | 23 | var id: Int { 24 | UUID().hashValue 25 | } 26 | 27 | } 28 | 29 | extension Visit { 30 | 31 | static func mock(withDate date: Date) -> Visit { 32 | Visit(locationName: "Apple Inc", 33 | tagColor: .randomColor, 34 | arrivalDate: date, 35 | departureDate: date.addingTimeInterval(60*60)) 36 | } 37 | 38 | static func mocks(start: Date, end: Date) -> [Visit] { 39 | currentCalendar.generateVisits( 40 | start: start, 41 | end: end) 42 | } 43 | 44 | } 45 | 46 | fileprivate let visitCountRange = 1...20 47 | 48 | private extension Calendar { 49 | 50 | func generateVisits(start: Date, end: Date) -> [Visit] { 51 | var visits = [Visit]() 52 | 53 | enumerateDates( 54 | startingAfter: start, 55 | matching: .everyDay, 56 | matchingPolicy: .nextTime) { date, _, stop in 57 | if let date = date { 58 | if date < end { 59 | for _ in 0.. { 20 | let exclusiveEndIndex = visitIndex + numberOfCellsInBlock 21 | guard visits.count > numberOfCellsInBlock && 22 | exclusiveEndIndex <= visits.count else { 23 | return visitIndex.. Double { 47 | let startOfDay = currentCalendar.startOfDay(for: date) 48 | return Double((visitsByDay[startOfDay]?.count ?? 0) + 3) / 15.0 49 | } 50 | 51 | func calendar(canSelectDate date: Date) -> Bool { 52 | let day = currentCalendar.dateComponents([.day], from: date).day! 53 | return day != 4 54 | } 55 | 56 | func calendar(viewForSelectedDate date: Date, dimensions size: CGSize) -> AnyView { 57 | let startOfDay = currentCalendar.startOfDay(for: date) 58 | return VisitsListView(visits: visitsByDay[startOfDay] ?? [], height: size.height).erased 59 | } 60 | 61 | } 62 | 63 | extension ExampleMonthlyCalendarView: MonthlyCalendarDelegate { 64 | 65 | func calendar(didSelectDay date: Date) { 66 | print("Selected date: \(date)") 67 | } 68 | 69 | func calendar(willDisplayMonth date: Date) { 70 | print("Will show month: \(date)") 71 | } 72 | 73 | } 74 | 75 | struct ExampleMonthlyCalendarView_Previews: PreviewProvider { 76 | static var previews: some View { 77 | ExampleMonthlyCalendarView(ascVisits: Visit.mocks(start: .daysFromToday(-365*2), end: .daysFromToday(365*2)), initialMonth: nil) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Example/Example/Individual Views/ExampleYearlyCalendarView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 4:49 PM - 6/14/20 2 | 3 | import ElegantCalendar 4 | import SwiftUI 5 | 6 | struct ExampleYearlyCalendarView: View { 7 | 8 | @ObservedObject private var calendarManager: YearlyCalendarManager 9 | 10 | let visitsByDay: [Date: [Visit]] 11 | 12 | @State private var calendarTheme: CalendarTheme = .royalBlue 13 | 14 | init(ascVisits: [Visit], initialYear: Date?) { 15 | let configuration = CalendarConfiguration(calendar: currentCalendar, 16 | startDate: ascVisits.first!.arrivalDate, 17 | endDate: ascVisits.last!.arrivalDate) 18 | calendarManager = YearlyCalendarManager(configuration: configuration, 19 | initialYear: initialYear) 20 | visitsByDay = Dictionary(grouping: ascVisits, by: { currentCalendar.startOfDay(for: $0.arrivalDate) }) 21 | 22 | calendarManager.delegate = self 23 | } 24 | 25 | var body: some View { 26 | ZStack { 27 | YearlyCalendarView(calendarManager: calendarManager) 28 | .theme(calendarTheme) 29 | VStack { 30 | Spacer() 31 | changeThemeButton 32 | .padding(.bottom, 50) 33 | } 34 | } 35 | } 36 | 37 | private var changeThemeButton: some View { 38 | ChangeThemeButton(calendarTheme: $calendarTheme) 39 | } 40 | 41 | } 42 | 43 | extension ExampleYearlyCalendarView: YearlyCalendarDelegate { 44 | 45 | func calendar(didSelectMonth date: Date) { 46 | print("Selected month: \(date)") 47 | } 48 | 49 | func calendar(willDisplayYear date: Date) { 50 | print("Will show year: \(date)") 51 | } 52 | 53 | } 54 | 55 | struct ExampleYearlyCalendarView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | ExampleYearlyCalendarView(ascVisits: Visit.mocks(start: .daysFromToday(-365*2), end: .daysFromToday(365*2)), initialYear: nil) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Example/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | 50 | UISupportedInterfaceOrientations~ipad 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationPortraitUpsideDown 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:33 PM - 6/24/20 2 | 3 | import UIKit 4 | import SwiftUI 5 | 6 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 7 | 8 | var window: UIWindow? 9 | 10 | 11 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 12 | if let windowScene = scene as? UIWindowScene { 13 | let window = UIWindow(windowScene: windowScene) 14 | window.rootViewController = UIHostingController(rootView: ContentView()) 15 | self.window = window 16 | window.makeKeyAndVisible() 17 | } 18 | } 19 | 20 | func sceneDidDisconnect(_ scene: UIScene) { 21 | // Called as the scene is being released by the system. 22 | // This occurs shortly after the scene enters the background, or when its session is discarded. 23 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 24 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 25 | } 26 | 27 | func sceneDidBecomeActive(_ scene: UIScene) { 28 | // Called when the scene has moved from an inactive state to an active state. 29 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 30 | } 31 | 32 | func sceneWillResignActive(_ scene: UIScene) { 33 | // Called when the scene will move from an active state to an inactive state. 34 | // This may occur due to temporary interruptions (ex. an incoming phone call). 35 | } 36 | 37 | func sceneWillEnterForeground(_ scene: UIScene) { 38 | // Called as the scene transitions from the background to the foreground. 39 | // Use this method to undo the changes made on entering the background. 40 | } 41 | 42 | func sceneDidEnterBackground(_ scene: UIScene) { 43 | // Called as the scene transitions from the foreground to the background. 44 | // Use this method to save data, release shared resources, and store enough scene-specific state information 45 | // to restore the scene back to its current state. 46 | } 47 | 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Example/Example/Selection and Exit/ExampleSelectionView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 8:33 PM - 6/24/20 2 | 3 | import ElegantPages 4 | import ElegantCalendar 5 | import SwiftUI 6 | 7 | fileprivate let turnAnimation: Animation = .spring(response: 0.4, dampingFraction: 0.95) 8 | 9 | class SelectionModel: ObservableObject { 10 | 11 | @Published var showCalendar = false 12 | @Published var calendarManager: ElegantCalendarManager = .init(configuration: .init(startDate: .daysFromToday(-365), 13 | endDate: .daysFromToday(365*3))) 14 | 15 | init() { 16 | calendarManager.delegate = self 17 | } 18 | 19 | } 20 | 21 | extension SelectionModel: ElegantCalendarDelegate { 22 | 23 | func calendar(didSelectDay date: Date) { 24 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 25 | withAnimation(turnAnimation) { 26 | self.showCalendar = false 27 | } 28 | } 29 | } 30 | 31 | } 32 | 33 | struct ExampleSelectionView: View { 34 | 35 | @ObservedObject var model: SelectionModel = .init() 36 | 37 | var calendarManager: ElegantCalendarManager { 38 | model.calendarManager 39 | } 40 | 41 | private var offset: CGFloat { 42 | model.showCalendar ? -screen.width : -screen.width*2 43 | } 44 | 45 | var body: some View { 46 | HStack(alignment: .center, spacing: 0) { 47 | calendarView 48 | .frame(width: screen.width*2, height: screen.height, alignment: .trailing) 49 | homeView 50 | .frame(width: screen.width, height: screen.height) 51 | } 52 | .frame(width: screen.width, height: screen.height, alignment: .leading) 53 | .offset(x: offset) 54 | } 55 | 56 | private var calendarView: some View { 57 | ElegantCalendarView(calendarManager: calendarManager) 58 | } 59 | 60 | private var homeView: some View { 61 | VStack(spacing: 25) { 62 | Button(action: { 63 | withAnimation(turnAnimation) { 64 | self.model.showCalendar = true 65 | } 66 | }) { 67 | Text("Show Calendar").foregroundColor(.blackPearl) 68 | } 69 | 70 | Text(calendarManager.selectedDate?.fullDate ?? "No date selected") 71 | } 72 | } 73 | 74 | } 75 | 76 | struct ExampleSelectionView_Previews: PreviewProvider { 77 | static var previews: some View { 78 | ExampleSelectionView() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Example/Example/Shared/ChangeThemeButton.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:42 PM - 7/14/20 2 | 3 | import ElegantCalendar 4 | import SwiftUI 5 | 6 | struct ChangeThemeButton: View { 7 | 8 | @Binding var calendarTheme: CalendarTheme 9 | 10 | var body: some View { 11 | Button(action: { 12 | self.calendarTheme = .randomTheme 13 | }) { 14 | Text("CHANGE THEME") 15 | } 16 | } 17 | 18 | } 19 | 20 | private extension CalendarTheme { 21 | 22 | static var randomTheme: CalendarTheme { 23 | let randomNumber = arc4random_uniform(UInt32(CalendarTheme.allThemes.count)) 24 | return CalendarTheme.allThemes[Int(randomNumber)] 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Example/Example/Shared/Color+Custom.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:45 PM - 7/14/20 2 | 3 | import SwiftUI 4 | 5 | extension Color { 6 | 7 | static let blackPearl = Color("blackPearl") 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Example/Example/Shared/Date+Additions.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 12:05 PM - 8/1/20 2 | 3 | import Foundation 4 | 5 | extension Date { 6 | 7 | static func daysFromToday(_ days: Int) -> Date { 8 | Date().addingTimeInterval(TimeInterval(60*60*24*days)) 9 | } 10 | 11 | } 12 | 13 | extension Date { 14 | 15 | var fullDate: String { 16 | DateFormatter.fullDate.string(from: self) 17 | } 18 | 19 | var timeOnlyWithPadding: String { 20 | DateFormatter.timeOnlyWithPadding.string(from: self) 21 | } 22 | 23 | } 24 | 25 | extension DateFormatter { 26 | 27 | static var fullDate: DateFormatter { 28 | let formatter = DateFormatter() 29 | formatter.dateFormat = "EEEE, MMM d, yyyy" 30 | return formatter 31 | } 32 | 33 | static let timeOnlyWithPadding: DateFormatter = { 34 | let formatter = DateFormatter() 35 | formatter.dateFormat = "h:mm a" 36 | return formatter 37 | }() 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Example/Example/Shared/LightDarkThemePreview.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 12:08 PM - 8/1/20 2 | 3 | import SwiftUI 4 | 5 | struct LightDarkThemePreview: View { 6 | 7 | let preview: Preview 8 | 9 | var body: some View { 10 | Group { 11 | LightThemePreview { 12 | self.preview 13 | } 14 | 15 | DarkThemePreview { 16 | self.preview 17 | } 18 | } 19 | } 20 | 21 | init(@ViewBuilder preview: @escaping () -> Preview) { 22 | self.preview = preview() 23 | } 24 | 25 | } 26 | 27 | struct LightThemePreview: View { 28 | 29 | let preview: Preview 30 | 31 | var body: some View { 32 | preview 33 | .previewLayout(.sizeThatFits) 34 | .colorScheme(.light) 35 | } 36 | 37 | init(@ViewBuilder preview: @escaping () -> Preview) { 38 | self.preview = preview() 39 | } 40 | 41 | } 42 | 43 | struct DarkThemePreview: View { 44 | 45 | let preview: Preview 46 | 47 | var body: some View { 48 | preview 49 | .previewLayout(.sizeThatFits) 50 | .colorScheme(.dark) 51 | .background(Color.black.edgesIgnoringSafeArea(.all)) 52 | } 53 | 54 | init(@ViewBuilder preview: @escaping () -> Preview) { 55 | self.preview = preview() 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kevin Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ElegantPages", 6 | "repositoryURL": "https://github.com/ThasianX/ElegantPages", 7 | "state": { 8 | "branch": null, 9 | "revision": "47d166799f2313ad9e47c9c22687021d396d940f", 10 | "version": "1.4.1" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ElegantCalendar", 7 | platforms: [ 8 | .iOS(.v13) 9 | ], 10 | products: [ 11 | .library( 12 | name: "ElegantCalendar", 13 | targets: ["ElegantCalendar"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/ThasianX/ElegantPages", from: "1.4.1") 17 | ], 18 | targets: [ 19 | .target( 20 | name: "ElegantCalendar", 21 | dependencies: ["ElegantPages"]) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElegantCalendar 2 | 3 |

4 | Platforms 5 | 6 | License: MIT 7 |

8 | 9 | ElegantCalendar is an efficient and customizable full screen calendar written in SwiftUI. 10 | 11 |
12 | 13 | 14 | 15 | ## [ElegantTimeline](https://github.com/ThasianX/ElegantTimeline-SwiftUI) - Shows what's possible using ElegantCalendar 16 | 17 | 18 | 19 | ### Comes with 8 default themes. You can also configure your own theme. Read more to find out. 20 | 21 |

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

30 | 31 | - [Introduction](#introduction) 32 | - [Basic Usage](#basic-usage) 33 | - [How It Works](#how-it-works) 34 | - [Customization](#customization) 35 | - [Use Cases](#use-cases) 36 | - [Demos](#demos) 37 | - [Installation](#installation) 38 | - [Requirements](#requirements) 39 | - [Contributing](#contributing) 40 | - [Resources](#resources) 41 | - [License](#license) 42 | 43 | ## Introduction 44 | 45 | `ElegantCalendar` is inspired by [TimePage](https://us.moleskine.com/timepage/p0486) and is part of a larger repository of elegant demonstrations like this: [TimePage Clone](https://github.com/ThasianX/TimePage-Clone). It uses [ElegantPages](https://github.com/ThasianX/ElegantPages), another library I wrote specifically for paging so check that out :) 46 | 47 | It is mainly meant to be used with apps that require the use of a calendar to function(like [ElegantTimeline](https://github.com/ThasianX/ElegantTimeline-SwiftUI)), not as a full screen date picker(the demo demonstrates how to do so if you really want to). 48 | 49 | Features: 50 | 51 | * Display months and years in a full screen vertical scrolling layout 52 | * Custom layout system that allows virtually infinite date ranges with minimal increasing memory usage 53 | * Customization of individual day views 54 | * Customization of the calendar color scheme, light and dark 55 | * Customization of the accessory view displayed when selecting a day 56 | * Excluding certain days from being selectable on the calendar 57 | * Scrolling to a particular day, month, or year with or without animation 58 | * Built in button that scrolls back to today’s month or year 59 | * Flexibility in either using the full calendar view that has both the monthly and yearly view or just one of the individual views 60 | * Haptics when performing certain actions 61 | * Intuitive navigation between the yearly and monthly view: swipe between views or tap on the month header to navigate to the yearly view 62 | * Elegant default themes 63 | 64 | 65 | ## Basic usage 66 | 67 | Using `ElegantCalendar` is as easy as: 68 | 69 | ```swift 70 | 71 | import ElegantCalendar 72 | 73 | struct ExampleCalendarView: View { 74 | 75 | // Start & End date should be configured based on your needs. 76 | let startDate = Date().addingTimeInterval(TimeInterval(60 * 60 * 24 * (-30 * 36))) 77 | let endDate = Date().addingTimeInterval(TimeInterval(60 * 60 * 24 * (30 * 36))) 78 | 79 | @ObservedObject var calendarManager = ElegantCalendarManager( 80 | configuration: CalendarConfiguration(startDate: startDate, 81 | endDate: endDate)) 82 | 83 | var body: some View { 84 | ElegantCalendarView(calendarManager: calendarManager) 85 | } 86 | 87 | } 88 | ``` 89 | 90 | However, if you just want an individual view, not the entire calendar view, you can do either: 91 | 92 | ```swift 93 | 94 | import ElegantCalendar 95 | 96 | struct ExampleMonthlyCalendarView: View { 97 | 98 | // Start & End date should be configured based on your needs. 99 | let startDate = Date().addingTimeInterval(TimeInterval(60 * 60 * 24 * (-30 * 36))) 100 | let endDate = Date().addingTimeInterval(TimeInterval(60 * 60 * 24 * (30 * 36))) 101 | 102 | @ObservedObject var calendarManager = MonthlyCalendarManager( 103 | configuration: CalendarConfiguration(startDate: startDate, 104 | endDate: endDate)) 105 | 106 | var body: some View { 107 | MonthlyCalendarView(calendarManager: calendarManager) 108 | } 109 | 110 | } 111 | 112 | struct ExampleYearlyCalendarView: View { 113 | 114 | // Start & End date should be configured based on your needs. 115 | let startDate = Date().addingTimeInterval(TimeInterval(60 * 60 * 24 * (-30 * 36))) 116 | let endDate = Date().addingTimeInterval(TimeInterval(60 * 60 * 24 * (30 * 36))) 117 | 118 | @ObservedObject var calendarManager = YearlyCalendarManager( 119 | configuration: CalendarConfiguration(startDate: startDate, 120 | endDate: endDate)) 121 | 122 | var body: some View { 123 | YearlyCalendarView(calendarManager: calendarManager) 124 | } 125 | 126 | } 127 | 128 | ``` 129 | 130 | ## How it works 131 | 132 | [`ElegantCalendarView`](https://github.com/ThasianX/ElegantCalendar/blob/master/Sources/ElegantCalendar/Views/ElegantCalendarView.swift) uses the [`ElegantHPages`](https://github.com/ThasianX/ElegantPages/blob/master/Sources/ElegantPages/Pages/Public/ElegantHPages.swift) view from [`ElegantPages`](https://github.com/ThasianX/ElegantPages). Essentially, it's just a swipable `HStack` that loads all the views immediately. And it's also for this reason that it is not recommended that `ElegantCalendarView` should not be used as a date picker. Here's why. 133 | 134 | Let's first talk about the monthly calendar where you can swipe up and down to see the next/previous month. This view uses [`ElegantVList`](https://github.com/ThasianX/ElegantPages/blob/master/Sources/ElegantPages/Lists/Public/ElegantVList.swift) and is really efficient memory and performance wise. When it comes to the yearly calendar, performance is just as amazing. However, the catch is that all the year views have to be loaded into memory and drawn onto the screen first. This takes a few seconds depending on your date range, the wider the longer. However, once this loading process is over, the calendar functions smoothly and elegantly. 135 | 136 | So how can this be fixed? Either create a simpler yearly calendar that doesn't require as much CoreGraphics drawing as the current one or load the year views on demand. The problem with the second approach is that SwiftUI is just inefficient at making views, as it spends a [LOT of CPU on rendering](https://github.com/warrenburton/DequeueOrNot). Hopefully, in future iterations of SwiftUI, the rendering becomes smoother. As for the former approach, it seems the most feasible and I will consider implementing it if enough people display interest. Just make an issue about it so I can tell. 137 | 138 | ## Customization 139 | 140 | ### `ElegantCalendarManager` 141 | 142 | #### `configuration`: The configuration of the calendar view 143 | 144 | ```swift 145 | 146 | public struct CalendarConfiguration: Equatable { 147 | 148 | let calendar: Calendar 149 | let ascending: Bool // reverses the order in which the calendar is laid out 150 | let startDate: Date 151 | let endDate: Date 152 | 153 | } 154 | 155 | ``` 156 | 157 | #### `initialMonth`: The initial month to display on the calendar. If not specified, automatically defaults to the first month. 158 | 159 | #### `datasource`: The datasource of the calendar 160 | 161 | ```swift 162 | 163 | public protocol ElegantCalendarDataSource: MonthlyCalendarDataSource, YearlyCalendarDataSource { } 164 | 165 | public protocol MonthlyCalendarDataSource { 166 | 167 | func calendar(backgroundColorOpacityForDate date: Date) -> Double 168 | func calendar(canSelectDate date: Date) -> Bool 169 | func calendar(viewForSelectedDate date: Date, dimensions size: CGSize) -> AnyView 170 | 171 | } 172 | 173 | public protocol YearlyCalendarDataSource { } 174 | 175 | ``` 176 | 177 | This allows you to customize the opacity of any given day, whether you want a day to be tappable or not, and the accessory view that shows when a day is tapped. 178 | 179 | #### `delegate`: The delegate of the calendar 180 | 181 | ```swift 182 | 183 | public protocol ElegantCalendarDelegate: MonthlyCalendarDelegate, YearlyCalendarDelegate { } 184 | 185 | public protocol MonthlyCalendarDelegate { 186 | 187 | func calendar(didSelectDay date: Date) 188 | func calendar(willDisplayMonth date: Date) 189 | 190 | } 191 | 192 | public protocol YearlyCalendarDelegate { 193 | 194 | func calendar(didSelectMonth date: Date) 195 | func calendar(willDisplayYear date: Date) 196 | 197 | } 198 | 199 | ``` 200 | 201 | This is just a convenience to handle the shortcomings of the `@Published` wrapper which doesn't support `didSet`. Conform to this if you need to do things when a month is displayed or date changes. 202 | 203 | #### `theme`: The theme of various components of the calendar. Default is royal blue. Available for `ElegantCalendarView` & `YearlyCalendarView` & `MonthlyCalendarView`. 204 | 205 | ```swift 206 | 207 | public struct CalendarTheme: Equatable, Hashable { 208 | 209 | let primary: Color 210 | 211 | } 212 | 213 | public extension CalendarTheme { 214 | 215 | static let brilliantViolet = CalendarTheme(primary: .brilliantViolet) 216 | static let craftBrown = CalendarTheme(primary: .craftBrown) 217 | static let fluorescentPink = CalendarTheme(primary: .fluorescentPink) 218 | static let kiwiGreen = CalendarTheme(primary: .kiwiGreen) 219 | static let mauvePurple = CalendarTheme(primary: .mauvePurple) 220 | static let orangeYellow = CalendarTheme(primary: .orangeYellow) 221 | static let red = CalendarTheme(primary: .red) 222 | static let royalBlue = CalendarTheme(primary: .royalBlue) 223 | 224 | } 225 | 226 | ElegantCalendarView(...) 227 | .theme(.mauvePurple) 228 | 229 | ``` 230 | 231 | To configure your own theme, just pass in your color into the `CalendarTheme` initializer. To have dynamic appearance, make sure your `Color` has both a light and dark appearance. 232 | 233 | #### `horizontal` or `vertical`: The orientation of the calendar. The default is `horizontal`, as shown in the GIF. Available for `ElegantCalendarView` & `YearlyCalendarView` & `MonthlyCalendarView`. 234 | 235 | ```swift 236 | 237 | ElegantCalendarView(...) 238 | .vertical() 239 | 240 | ``` 241 | 242 | #### `allowsHaptics`: Whether haptics is enabled or not. Default is enabled. Available for `ElegantCalendarView` & `MonthlyCalendarView` 243 | 244 | ```swift 245 | 246 | ElegantCalendarView(...) 247 | .allowsHaptics(false) 248 | 249 | ``` 250 | 251 | Users get haptics whenever they tap a day, scroll to a new month, or press the scroll back to today button. 252 | 253 | #### `frame`: Custom width for the monthly calendar view. Available for `MonthlyCalendarView` 254 | 255 | ```swift 256 | 257 | MonthlyCalendarView(...) 258 | .frame(width: ...) 259 | 260 | ``` 261 | 262 | ## Use Cases 263 | 264 | The following aspects of `ElegantCalendarManager` can be used: 265 | 266 | `var currentMonth: Date` - The current month displayed on the calendar view. 267 | 268 | `var selectedDate: Date?` - The date selected on the calendar view, if any. 269 | 270 | `var isShowingYearView: Bool` - Whether the year view is showing. If false, the month view is showing. 271 | 272 | `func scrollToMonth(_ month: Date, animated: Bool = true)` - Scroll back to a certain month, animated or not. No date is selected in the process. 273 | 274 | `func scrollBackToToday(animated: Bool = true)` - Scroll back to today, animated or not. Today's date is selected in the process. 275 | 276 | `func scrollToDay(_ day: Date, animated: Bool = true)` - Scroll back to a certain date, animated or not. The date is selected in the process. 277 | 278 | ## Demos 279 | 280 | The demos shown in the GIF can be checked out on [example repo](https://github.com/ThasianX/ElegantCalendar/tree/master/Example). 281 | 282 | ## Installation 283 | 284 | `ElegantCalendar` is available using the [Swift Package Manager](https://swift.org/package-manager/): 285 | 286 | Using Xcode 11, go to `File -> Swift Packages -> Add Package Dependency` and enter https://github.com/ThasianX/ElegantCalendar 287 | 288 | If you are using `Package.swift`, you can also add `ElegantCalendar` as a dependency easily. 289 | 290 | ```swift 291 | 292 | let package = Package( 293 | name: "TestProject", 294 | dependencies: [ 295 | .package(url: "https://github.com/ThasianX/ElegantCalendar", from: "4.2.0") 296 | ], 297 | targets: [ 298 | .target(name: "TestProject", dependencies: ["ElegantCalendar"]) 299 | ] 300 | ) 301 | 302 | ``` 303 | 304 | Inside whatever app is using `ElegantCalendar` or your `Swift Package` that uses `ElegantCalendar` as a dependency: 305 | 306 | 1) Scroll the project navigator down to the `Swift Package Dependencies` section. Inside `ElegantCalendar`, you'll see a directory called `ElegantCalendar.xcassets`. 307 | 2) After you've located it, open your project's settings and navigate to your target's build phases in a parallel window. 308 | 3) Drag `ElegantCalendar.xcassets` into your target's `Copy Bundle Resources`. Make sure that `Copy items if needed` is unticked and `Create groups` is ticked. This step is crucial because `ElegantCalendar` uses custom icons, which `SPM` will support in [Swift 5.3](https://github.com/apple/swift-evolution/blob/master/proposals/0271-package-manager-resources.md). 309 | 4) This last step is for making sure that when others clone your repository, the assets will be available to them as well. Click the `ElegantCalendar.xcassets` that has appeared in your project navigator and in the inspector on the right, select `Identity and Type`. Inside, make sure that `Location` is set to `Relative to Build Products`. 310 | 311 | If you don't know how to do this, refer to the `Demo`. 312 | 313 | ## Requirements 314 | 315 | - iOS 13.0+ 316 | - Xcode 11.0+ 317 | 318 | ## Contributing 319 | 320 | If you find a bug, or would like to suggest a new feature or enhancement, it'd be nice if you could [search the issue tracker](https://github.com/ThasianX/ElegantCalendar/issues) first; while we don't mind duplicates, keeping issues unique helps us save time and considates effort. If you can't find your issue, feel free to [file a new one](https://github.com/ThasianX/ElegantCalendar/issues/new). 321 | 322 | ## Resources 323 | 324 | Also, here's a [dump of resources](resources.txt) I found useful when working on this 325 | 326 | ## License 327 | 328 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 329 | -------------------------------------------------------------------------------- /Screenshots/brilliantViolet.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThasianX/ElegantCalendar/3d1e8369ffb233e392ee36ad6ff0fd38ad4846b2/Screenshots/brilliantViolet.PNG -------------------------------------------------------------------------------- /Screenshots/craftBrown.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThasianX/ElegantCalendar/3d1e8369ffb233e392ee36ad6ff0fd38ad4846b2/Screenshots/craftBrown.PNG -------------------------------------------------------------------------------- /Screenshots/fluorescentPink.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThasianX/ElegantCalendar/3d1e8369ffb233e392ee36ad6ff0fd38ad4846b2/Screenshots/fluorescentPink.PNG -------------------------------------------------------------------------------- /Screenshots/kiwiGreen.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThasianX/ElegantCalendar/3d1e8369ffb233e392ee36ad6ff0fd38ad4846b2/Screenshots/kiwiGreen.PNG -------------------------------------------------------------------------------- /Screenshots/mauvePurple.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThasianX/ElegantCalendar/3d1e8369ffb233e392ee36ad6ff0fd38ad4846b2/Screenshots/mauvePurple.PNG -------------------------------------------------------------------------------- /Screenshots/orangeYellow.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThasianX/ElegantCalendar/3d1e8369ffb233e392ee36ad6ff0fd38ad4846b2/Screenshots/orangeYellow.PNG -------------------------------------------------------------------------------- /Screenshots/red.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThasianX/ElegantCalendar/3d1e8369ffb233e392ee36ad6ff0fd38ad4846b2/Screenshots/red.PNG -------------------------------------------------------------------------------- /Screenshots/royalblue.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThasianX/ElegantCalendar/3d1e8369ffb233e392ee36ad6ff0fd38ad4846b2/Screenshots/royalblue.PNG -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Extensions/Axis+Invert.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 12:07 PM - 8/4/20 2 | 3 | import SwiftUI 4 | 5 | extension Axis { 6 | 7 | var inverted: Axis { 8 | (self == .vertical) ? .horizontal : .vertical 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Extensions/Calender+Dates.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:28 PM - 6/10/20 2 | 3 | import SwiftUI 4 | 5 | extension Calendar { 6 | 7 | func endOfDay(for date: Date) -> Date { 8 | var components = DateComponents() 9 | components.day = 1 10 | components.second = -1 11 | return self.date(byAdding: components, to: startOfDay(for: date))! 12 | } 13 | 14 | func isDate(_ date1: Date, equalTo date2: Date, toGranularities components: Set) -> Bool { 15 | components.reduce(into: true) { isEqual, component in 16 | isEqual = isEqual && isDate(date1, equalTo: date2, toGranularity: component) 17 | } 18 | } 19 | 20 | func startOfMonth(for date: Date) -> Date { 21 | let components = dateComponents([.month, .year], from: date) 22 | return self.date(from: components)! 23 | } 24 | 25 | func startOfYear(for date: Date) -> Date { 26 | let components = dateComponents([.year], from: date) 27 | return self.date(from: components)! 28 | } 29 | 30 | } 31 | 32 | extension Calendar { 33 | 34 | func generateDates(inside interval: DateInterval, 35 | matching components: DateComponents) -> [Date] { 36 | var dates: [Date] = [] 37 | dates.append(interval.start) 38 | 39 | enumerateDates( 40 | startingAfter: interval.start, 41 | matching: components, 42 | matchingPolicy: .nextTime) { date, _, stop in 43 | if let date = date { 44 | if date < interval.end { 45 | dates.append(date) 46 | } else { 47 | stop = true 48 | } 49 | } 50 | } 51 | 52 | return dates 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Extensions/Color+CustomColors.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 2:46 PM - 6/1/20 2 | 3 | import SwiftUI 4 | 5 | extension Color { 6 | 7 | static let systemBackground = Color(UIColor.systemBackground) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Extensions/Date+DaysFromToday.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 1:54 PM - 6/13/20 2 | 3 | import Foundation 4 | 5 | extension Date { 6 | 7 | static func daysFromToday(_ days: Int) -> Date { 8 | Date().addingTimeInterval(TimeInterval(60*60*24*days)) 9 | } 10 | 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Extensions/Date+toString.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:09 PM - 6/6/20 2 | 3 | import Foundation 4 | 5 | extension Date { 6 | 7 | var abbreviatedMonth: String { 8 | DateFormatter.abbreviatedMonth.string(from: self) 9 | } 10 | 11 | var dayOfWeekWithMonthAndDay: String { 12 | DateFormatter.dayOfWeekWithMonthAndDay.string(from: self) 13 | } 14 | 15 | var fullMonth: String { 16 | DateFormatter.fullMonth.string(from: self) 17 | } 18 | 19 | var timeOnlyWithPadding: String { 20 | DateFormatter.timeOnlyWithPadding.string(from: self) 21 | } 22 | 23 | var year: String { 24 | DateFormatter.year.string(from: self) 25 | } 26 | 27 | } 28 | 29 | extension DateFormatter { 30 | 31 | static var abbreviatedMonth: DateFormatter { 32 | let formatter = DateFormatter() 33 | formatter.dateFormat = "MMM" 34 | return formatter 35 | } 36 | 37 | static var dayOfWeekWithMonthAndDay: DateFormatter { 38 | let formatter = DateFormatter() 39 | formatter.dateFormat = "EEEE MMMM d" 40 | return formatter 41 | } 42 | 43 | static var fullMonth: DateFormatter { 44 | let formatter = DateFormatter() 45 | formatter.dateFormat = "MMMM" 46 | return formatter 47 | } 48 | 49 | static let timeOnlyWithPadding: DateFormatter = { 50 | let formatter = DateFormatter() 51 | formatter.dateFormat = "h:mm a" 52 | return formatter 53 | }() 54 | 55 | static var year: DateFormatter { 56 | let formatter = DateFormatter() 57 | formatter.dateFormat = "yyyy" 58 | return formatter 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Extensions/Enumeration+Matching.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 1:58 PM - 6/13/20 2 | 3 | import Foundation 4 | 5 | extension Calendar { 6 | 7 | var firstDayOfEveryWeek: DateComponents { 8 | DateComponents(hour: 0, minute: 0, second: 0, weekday: firstWeekday) 9 | } 10 | 11 | } 12 | 13 | extension DateComponents { 14 | 15 | static var everyDay: DateComponents { 16 | DateComponents(hour: 0, minute: 0, second: 0) 17 | } 18 | 19 | static var firstDayOfEveryMonth: DateComponents { 20 | DateComponents(day: 1, hour: 0, minute: 0, second: 0) 21 | } 22 | 23 | static var firstDayOfEveryYear: DateComponents { 24 | DateComponents(month: 1, day: 1, hour: 0, minute: 0, second: 0) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Extensions/EnvironmentKey+CalendarTheme.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:10 PM - 7/14/20 2 | 3 | import SwiftUI 4 | 5 | public struct CalendarTheme: Equatable, Hashable { 6 | 7 | public let primary: Color 8 | public let titleColor: Color 9 | public let textColor: Color 10 | public let todayTextColor: Color 11 | public let todayBackgroundColor: Color 12 | 13 | public init(primary: Color, titleColor: Color? = nil, textColor: Color? = nil, todayTextColor: Color? = nil, todayBackgroundColor: Color? = nil) { 14 | self.primary = primary 15 | 16 | if let titleColor = titleColor { self.titleColor = titleColor } else { self.titleColor = primary } 17 | if let textColor = textColor { self.textColor = textColor } else { self.textColor = .primary } 18 | if let todayTextColor = todayTextColor { self.todayTextColor = todayTextColor } else { self.todayTextColor = primary } 19 | if let todayBackgroundColor = todayBackgroundColor { self.todayBackgroundColor = todayBackgroundColor } else { self.todayBackgroundColor = .primary } 20 | } 21 | 22 | } 23 | 24 | public extension CalendarTheme { 25 | 26 | static let allThemes: [CalendarTheme] = [.brilliantViolet, .craftBrown, .fluorescentPink, .kiwiGreen, .mauvePurple, .orangeYellow, .red, .royalBlue] 27 | 28 | static let brilliantViolet = CalendarTheme(primary: .brilliantViolet) 29 | static let craftBrown = CalendarTheme(primary: .craftBrown) 30 | static let fluorescentPink = CalendarTheme(primary: .fluorescentPink) 31 | static let kiwiGreen = CalendarTheme(primary: .kiwiGreen) 32 | static let mauvePurple = CalendarTheme(primary: .mauvePurple) 33 | static let orangeYellow = CalendarTheme(primary: .orangeYellow) 34 | static let red = CalendarTheme(primary: .red) 35 | static let royalBlue = CalendarTheme(primary: .royalBlue) 36 | 37 | } 38 | 39 | extension CalendarTheme { 40 | 41 | static let `default`: CalendarTheme = .royalBlue 42 | 43 | } 44 | 45 | struct CalendarThemeKey: EnvironmentKey { 46 | 47 | static let defaultValue: CalendarTheme = .default 48 | 49 | } 50 | 51 | extension EnvironmentValues { 52 | 53 | var calendarTheme: CalendarTheme { 54 | get { self[CalendarThemeKey.self] } 55 | set { self[CalendarThemeKey.self] = newValue } 56 | } 57 | 58 | } 59 | 60 | private extension Color { 61 | 62 | static let brilliantViolet = Color("brilliantViolet") 63 | static let craftBrown = Color("craftBrown") 64 | static let fluorescentPink = Color("fluorescentPink") 65 | static let kiwiGreen = Color("kiwiGreen") 66 | static let mauvePurple = Color("mauvePurple") 67 | static let orangeYellow = Color("orangeYellow") 68 | static let red = Color("red") 69 | static let royalBlue = Color("royalBlue") 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Extensions/Image+Custom.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 3:24 PM - 7/13/20 2 | 3 | import SwiftUI 4 | 5 | extension Image { 6 | 7 | static var uTurnLeft: Image = { 8 | guard let image = UIImage(named: "uturn.left") else { 9 | fatalError("Error: `ElegantCalendar.xcassets` doesn't exist. Refer to the `README.md` installation on how to resolve this.") 10 | } 11 | return Image(uiImage: image) 12 | }() 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Extensions/UIImage+BundleInit.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:44 PM - 7/13/20 2 | 3 | import UIKit 4 | 5 | fileprivate class BundleId {} 6 | 7 | extension UIImage { 8 | 9 | internal convenience init?(named: String) { 10 | let customBundle = Bundle(for: BundleId.self) 11 | self.init(named: named, in: customBundle, compatibleWith: nil) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Extensions/UIImpactFeedbackGenerator+Haptic.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 8:58 PM - 7/9/20 2 | 3 | import UIKit 4 | 5 | extension UIImpactFeedbackGenerator { 6 | 7 | private static var selectionHaptic = UISelectionFeedbackGenerator() 8 | static func generateSelectionHaptic() { 9 | selectionHaptic.selectionChanged() 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/CalendarConfiguration.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 10:51 PM - 6/6/20 2 | 3 | import SwiftUI 4 | 5 | /// Any changes to the configuration will reset the calendar based on its new settings 6 | public struct CalendarConfiguration: Equatable { 7 | 8 | public let calendar: Calendar 9 | public let ascending: Bool 10 | public let startDate: Date 11 | public let endDate: Date 12 | 13 | public init(calendar: Calendar = .current, ascending: Bool = true, startDate: Date, endDate: Date) { 14 | self.calendar = calendar 15 | self.ascending = ascending 16 | self.startDate = startDate 17 | self.endDate = endDate 18 | } 19 | 20 | var referenceDate: Date { 21 | ascending ? startDate : endDate 22 | } 23 | 24 | } 25 | 26 | extension CalendarConfiguration { 27 | 28 | static let mock = CalendarConfiguration( 29 | startDate: .daysFromToday(-365*2), 30 | endDate: .daysFromToday(365*2)) 31 | 32 | } 33 | 34 | protocol ConfigurationDirectAccess { 35 | 36 | var configuration: CalendarConfiguration { get } 37 | 38 | } 39 | 40 | extension ConfigurationDirectAccess { 41 | 42 | var calendar: Calendar { 43 | configuration.calendar 44 | } 45 | 46 | var startDate: Date { 47 | configuration.startDate 48 | } 49 | 50 | var endDate: Date { 51 | configuration.endDate 52 | } 53 | 54 | var referenceDate: Date { 55 | configuration.referenceDate 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/CalenderConstants.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:00 AM - 6/7/20 2 | 3 | import SwiftUI 4 | 5 | let screen = UIScreen.main.bounds 6 | 7 | struct CalendarConstants { 8 | 9 | static let cellHeight: CGFloat = screen.height 10 | 11 | static let daysInRow: CGFloat = 7 12 | 13 | struct Monthly { 14 | 15 | static var cellWidth: CGFloat! 16 | static let horizontalPadding: CGFloat = cellWidth * 0.045 17 | 18 | static let outerHorizontalPadding: CGFloat = horizontalPadding + dayWidth/4 19 | 20 | static let topPadding: CGFloat = cellHeight * 0.078 21 | static let gridSpacing: CGFloat = cellWidth * 0.038 22 | 23 | static let dayWidth: CGFloat = { 24 | let totalHorizontalPadding: CGFloat = 2 * horizontalPadding 25 | let innerGridSpacing: CGFloat = (daysInRow - 1) * gridSpacing 26 | return (cellWidth - totalHorizontalPadding - innerGridSpacing) / daysInRow 27 | }() 28 | 29 | } 30 | 31 | struct Yearly { 32 | 33 | static let cellWidth: CGFloat = screen.width 34 | static let horizontalPadding: CGFloat = cellWidth * 0.058 35 | 36 | static let outerHorizontalPadding: CGFloat = horizontalPadding + monthWidth/7 37 | 38 | static let topPadding: CGFloat = cellHeight * 0.12 39 | 40 | static let monthsInRow = 3 41 | static let monthsInColumn = 4 42 | static let monthsGridSpacing: CGFloat = 4 43 | static let monthWidth: CGFloat = { 44 | let totalHorizontalPadding: CGFloat = 2 * horizontalPadding 45 | let innerGridSpacing: CGFloat = CGFloat(monthsInRow - 1) * monthsGridSpacing 46 | return (cellWidth - totalHorizontalPadding - innerGridSpacing) / CGFloat(monthsInRow) 47 | }() 48 | 49 | static let daysGridVerticalSpacing: CGFloat = 4 50 | static let daysGridHorizontalSpacing: CGFloat = 2 51 | static let dayWidth: CGFloat = { 52 | let innerGridSpacing: CGFloat = (daysInRow - 1) * daysGridHorizontalSpacing 53 | return (monthWidth - innerGridSpacing) / daysInRow 54 | }() 55 | static let daysStackHeight: CGFloat = 6*dayWidth + 5*daysGridVerticalSpacing 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/ObservableObjects/ElegantCalendarManager.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 5:25 PM - 6/10/20 2 | 3 | import Combine 4 | import ElegantPages 5 | import SwiftUI 6 | 7 | public class ElegantCalendarManager: ObservableObject { 8 | 9 | public var currentMonth: Date { 10 | monthlyManager.currentMonth 11 | } 12 | 13 | public var selectedDate: Date? { 14 | monthlyManager.selectedDate 15 | } 16 | 17 | public var isShowingYearView: Bool { 18 | pagesManager.currentPage == 0 19 | } 20 | 21 | @Published public var datasource: ElegantCalendarDataSource? 22 | @Published public var delegate: ElegantCalendarDelegate? 23 | 24 | public let configuration: CalendarConfiguration 25 | 26 | @Published public var yearlyManager: YearlyCalendarManager 27 | @Published public var monthlyManager: MonthlyCalendarManager 28 | 29 | let pagesManager: ElegantPagesManager 30 | 31 | private var anyCancellable = Set() 32 | 33 | public init(configuration: CalendarConfiguration, initialMonth: Date? = nil) { 34 | self.configuration = configuration 35 | 36 | yearlyManager = YearlyCalendarManager(configuration: configuration, 37 | initialYear: initialMonth) 38 | monthlyManager = MonthlyCalendarManager(configuration: configuration, 39 | initialMonth: initialMonth) 40 | 41 | pagesManager = ElegantPagesManager(startingPage: 1, 42 | pageTurnType: .calendarEarlySwipe) 43 | 44 | yearlyManager.communicator = self 45 | monthlyManager.communicator = self 46 | 47 | $datasource 48 | .sink { 49 | self.monthlyManager.datasource = $0 50 | self.yearlyManager.datasource = $0 51 | } 52 | .store(in: &anyCancellable) 53 | 54 | $delegate 55 | .sink { 56 | self.monthlyManager.delegate = $0 57 | self.yearlyManager.delegate = $0 58 | } 59 | .store(in: &anyCancellable) 60 | 61 | Publishers.CombineLatest(yearlyManager.objectWillChange, monthlyManager.objectWillChange) 62 | .sink { _ in self.objectWillChange.send() } 63 | .store(in: &anyCancellable) 64 | } 65 | 66 | public func scrollToMonth(_ month: Date, animated: Bool = true) { 67 | monthlyManager.scrollToMonth(month, animated: animated) 68 | } 69 | 70 | public func scrollBackToToday(animated: Bool = true) { 71 | scrollToDay(Date(), animated: animated) 72 | } 73 | 74 | public func scrollToDay(_ day: Date, animated: Bool = true) { 75 | monthlyManager.scrollToDay(day, animated: animated) 76 | } 77 | 78 | } 79 | 80 | extension ElegantCalendarManager { 81 | 82 | // accounts for both when the user scrolls to the yearly calendar view and the 83 | // user presses the month text to scroll to the yearly calendar view 84 | func scrollToYearIfOnYearlyView(_ page: Int) { 85 | if page == 0 { 86 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { 87 | self.yearlyManager.scrollToYear(self.currentMonth) 88 | } 89 | } 90 | } 91 | 92 | } 93 | 94 | extension ElegantCalendarManager: ElegantCalendarCommunicator { 95 | 96 | public func scrollToMonthAndShowMonthlyView(_ month: Date) { 97 | pagesManager.scroll(to: 1) 98 | 99 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { 100 | self.scrollToMonth(month) 101 | } 102 | } 103 | 104 | public func showYearlyView() { 105 | pagesManager.scroll(to: 0) 106 | } 107 | 108 | } 109 | 110 | protocol ElegantCalendarDirectAccess { 111 | 112 | var parent: ElegantCalendarManager? { get } 113 | 114 | } 115 | 116 | extension ElegantCalendarDirectAccess { 117 | 118 | var datasource: ElegantCalendarDataSource? { 119 | parent?.datasource 120 | } 121 | 122 | var delegate: ElegantCalendarDelegate? { 123 | parent?.delegate 124 | } 125 | 126 | } 127 | 128 | private extension PageTurnType { 129 | 130 | static let calendarEarlySwipe: PageTurnType = .earlyCutoff( 131 | config: .init(scrollResistanceCutOff: 40, 132 | pageTurnCutOff: 90, 133 | pageTurnAnimation: .interactiveSpring(response: 0.35, dampingFraction: 0.86, blendDuration: 0.25))) 134 | 135 | } 136 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/ObservableObjects/MonthlyCalendarManager.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 5:20 PM - 6/14/20 2 | 3 | import Combine 4 | import ElegantPages 5 | import SwiftUI 6 | 7 | public class MonthlyCalendarManager: ObservableObject, ConfigurationDirectAccess { 8 | 9 | @Published public private(set) var currentMonth: Date 10 | @Published public var selectedDate: Date? = nil 11 | 12 | let listManager: ElegantListManager 13 | 14 | @Published public var datasource: MonthlyCalendarDataSource? 15 | @Published public var delegate: MonthlyCalendarDelegate? 16 | 17 | public var communicator: ElegantCalendarCommunicator? 18 | 19 | public let configuration: CalendarConfiguration 20 | public let months: [Date] 21 | 22 | var allowsHaptics: Bool = true 23 | private var isHapticActive: Bool = true 24 | 25 | private var anyCancellable: AnyCancellable? 26 | 27 | public init(configuration: CalendarConfiguration, initialMonth: Date? = nil) { 28 | self.configuration = configuration 29 | 30 | let months = configuration.calendar.generateDates( 31 | inside: DateInterval(start: configuration.startDate, 32 | end: configuration.calendar.endOfDay(for: configuration.endDate)), 33 | matching: .firstDayOfEveryMonth) 34 | 35 | self.months = configuration.ascending ? months : months.reversed() 36 | 37 | var startingPage: Int = 0 38 | if let initialMonth = initialMonth { 39 | startingPage = configuration.calendar.monthsBetween(configuration.referenceDate, and: initialMonth) 40 | } 41 | 42 | currentMonth = months[startingPage] 43 | 44 | listManager = .init(startingPage: startingPage, 45 | pageCount: months.count) 46 | 47 | anyCancellable = $delegate.sink { 48 | $0?.calendar(willDisplayMonth: self.currentMonth) 49 | } 50 | } 51 | 52 | } 53 | 54 | extension MonthlyCalendarManager { 55 | 56 | func configureNewMonth(at page: Int) { 57 | if months[page] != currentMonth { 58 | currentMonth = months[page] 59 | selectedDate = nil 60 | 61 | delegate?.calendar(willDisplayMonth: currentMonth) 62 | 63 | if allowsHaptics && isHapticActive { 64 | UIImpactFeedbackGenerator.generateSelectionHaptic() 65 | } else { 66 | isHapticActive = true 67 | } 68 | } 69 | } 70 | 71 | } 72 | 73 | extension MonthlyCalendarManager { 74 | 75 | @discardableResult 76 | public func scrollBackToToday() -> Bool { 77 | scrollToDay(Date()) 78 | } 79 | 80 | @discardableResult 81 | public func scrollToDay(_ day: Date, animated: Bool = true) -> Bool { 82 | let didScrollToMonth = scrollToMonth(day, animated: animated) 83 | let canSelectDay = datasource?.calendar(canSelectDate: day) ?? true 84 | 85 | if canSelectDay { 86 | DispatchQueue.main.asyncAfter(deadline: .now()+0.15) { 87 | self.dayTapped(day: day, withHaptic: !didScrollToMonth) 88 | } 89 | } 90 | 91 | return canSelectDay 92 | } 93 | 94 | func dayTapped(day: Date, withHaptic: Bool) { 95 | if allowsHaptics && withHaptic { 96 | UIImpactFeedbackGenerator.generateSelectionHaptic() 97 | } 98 | 99 | selectedDate = day 100 | delegate?.calendar(didSelectDay: day) 101 | } 102 | 103 | @discardableResult 104 | public func scrollToMonth(_ month: Date, animated: Bool = true) -> Bool { 105 | isHapticActive = animated 106 | 107 | let needsToScroll = !calendar.isDate(currentMonth, equalTo: month, toGranularities: [.month, .year]) 108 | 109 | if needsToScroll { 110 | let page = calendar.monthsBetween(referenceDate, and: month) 111 | listManager.scroll(to: page, animated: animated) 112 | } else { 113 | isHapticActive = true 114 | } 115 | 116 | return needsToScroll 117 | } 118 | 119 | } 120 | 121 | extension MonthlyCalendarManager { 122 | 123 | static let mock = MonthlyCalendarManager(configuration: .mock) 124 | static let mockWithInitialMonth = MonthlyCalendarManager(configuration: .mock, initialMonth: .daysFromToday(60)) 125 | 126 | } 127 | 128 | protocol MonthlyCalendarManagerDirectAccess: ConfigurationDirectAccess { 129 | 130 | var calendarManager: MonthlyCalendarManager { get } 131 | var configuration: CalendarConfiguration { get } 132 | 133 | } 134 | 135 | extension MonthlyCalendarManagerDirectAccess { 136 | 137 | var configuration: CalendarConfiguration { 138 | calendarManager.configuration 139 | } 140 | 141 | var listManager: ElegantListManager { 142 | calendarManager.listManager 143 | } 144 | 145 | var months: [Date] { 146 | calendarManager.months 147 | } 148 | 149 | var communicator: ElegantCalendarCommunicator? { 150 | calendarManager.communicator 151 | } 152 | 153 | var datasource: MonthlyCalendarDataSource? { 154 | calendarManager.datasource 155 | } 156 | 157 | var delegate: MonthlyCalendarDelegate? { 158 | calendarManager.delegate 159 | } 160 | 161 | var currentMonth: Date { 162 | calendarManager.currentMonth 163 | } 164 | 165 | var selectedDate: Date? { 166 | calendarManager.selectedDate 167 | } 168 | 169 | func configureNewMonth(at page: Int) { 170 | calendarManager.configureNewMonth(at: page) 171 | } 172 | 173 | func scrollBackToToday() { 174 | calendarManager.scrollBackToToday() 175 | } 176 | 177 | } 178 | 179 | private extension Calendar { 180 | 181 | func monthsBetween(_ date1: Date, and date2: Date) -> Int { 182 | let startOfMonthForDate1 = startOfMonth(for: date1) 183 | let startOfMonthForDate2 = startOfMonth(for: date2) 184 | return abs(dateComponents([.month], 185 | from: startOfMonthForDate1, 186 | to: startOfMonthForDate2).month!) 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/ObservableObjects/PagerState.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 2:42 PM - 7/12/20 2 | 3 | import SwiftUI 4 | 5 | class PagerState: ObservableObject { 6 | 7 | @Published var activeIndex: Int = 1 8 | @Published var translation: CGFloat = .zero 9 | 10 | let pagerWidth: CGFloat 11 | 12 | init(pagerWidth: CGFloat) { 13 | self.pagerWidth = pagerWidth 14 | } 15 | 16 | } 17 | 18 | protocol PagerStateDirectAccess { 19 | 20 | var pagerState: PagerState { get } 21 | 22 | } 23 | 24 | extension PagerStateDirectAccess { 25 | 26 | var pagerWidth: CGFloat { 27 | pagerState.pagerWidth 28 | } 29 | 30 | var activeIndex: Int { 31 | pagerState.activeIndex 32 | } 33 | 34 | var translation: CGFloat { 35 | pagerState.translation 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/ObservableObjects/YearlyCalendarManager.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 5:19 PM - 6/14/20 2 | 3 | import Combine 4 | import SwiftUI 5 | 6 | public class YearlyCalendarManager: ObservableObject, ConfigurationDirectAccess { 7 | 8 | enum PageState { 9 | case scroll 10 | case completed 11 | } 12 | 13 | @Published var currentPage: (index: Int, state: PageState) = (0, .completed) 14 | 15 | public var currentYear: Date { 16 | years[currentPage.index] 17 | } 18 | 19 | @Published public var datasource: YearlyCalendarDataSource? 20 | @Published public var delegate: YearlyCalendarDelegate? 21 | 22 | public var communicator: ElegantCalendarCommunicator? 23 | 24 | public let configuration: CalendarConfiguration 25 | public let years: [Date] 26 | 27 | private var anyCancellable: AnyCancellable? 28 | 29 | public init(configuration: CalendarConfiguration, initialYear: Date? = nil) { 30 | self.configuration = configuration 31 | 32 | let years = configuration.calendar.generateDates( 33 | inside: DateInterval(start: configuration.startDate, 34 | end: configuration.endDate), 35 | matching: .firstDayOfEveryYear) 36 | 37 | self.years = configuration.ascending ? years : years.reversed() 38 | 39 | if let initialYear = initialYear { 40 | let page = calendar.yearsBetween(referenceDate, and: initialYear) 41 | currentPage = (page, .scroll) 42 | } else { 43 | anyCancellable = $delegate.sink { 44 | $0?.calendar(willDisplayYear: self.currentYear) 45 | } 46 | } 47 | } 48 | 49 | } 50 | 51 | extension YearlyCalendarManager { 52 | 53 | public func scrollBackToToday() { 54 | scrollToYear(Date()) 55 | } 56 | 57 | public func scrollToYear(_ year: Date) { 58 | if !calendar.isDate(currentYear, equalTo: year, toGranularity: .year) { 59 | let page = calendar.yearsBetween(referenceDate, and: year) 60 | currentPage = (page, .scroll) 61 | } 62 | } 63 | 64 | func willDisplay(page: Int) { 65 | if currentPage.index != page || currentPage.state == .scroll { 66 | currentPage = (page, .completed) 67 | delegate?.calendar(willDisplayYear: currentYear) 68 | } 69 | } 70 | 71 | func monthTapped(_ month: Date) { 72 | delegate?.calendar(didSelectMonth: month) 73 | communicator?.scrollToMonthAndShowMonthlyView(month) 74 | } 75 | 76 | } 77 | 78 | extension YearlyCalendarManager { 79 | 80 | static let mock = YearlyCalendarManager(configuration: .mock) 81 | static let mockWithInitialYear = YearlyCalendarManager(configuration: .mock, initialYear: .daysFromToday(365)) 82 | 83 | } 84 | 85 | protocol YearlyCalendarManagerDirectAccess: ConfigurationDirectAccess { 86 | 87 | var calendarManager: YearlyCalendarManager { get } 88 | var configuration: CalendarConfiguration { get } 89 | 90 | } 91 | 92 | extension YearlyCalendarManagerDirectAccess { 93 | 94 | var configuration: CalendarConfiguration { 95 | calendarManager.configuration 96 | } 97 | 98 | var communicator: ElegantCalendarCommunicator? { 99 | calendarManager.communicator 100 | } 101 | 102 | var datasource: YearlyCalendarDataSource? { 103 | calendarManager.datasource 104 | } 105 | 106 | var delegate: YearlyCalendarDelegate? { 107 | calendarManager.delegate 108 | } 109 | 110 | var currentYear: Date { 111 | calendarManager.currentYear 112 | } 113 | 114 | var years: [Date] { 115 | calendarManager.years 116 | } 117 | 118 | } 119 | 120 | private extension Calendar { 121 | 122 | func yearsBetween(_ date1: Date, and date2: Date) -> Int { 123 | let startOfYearForDate1 = startOfYear(for: date1) 124 | let startOfYearForDate2 = startOfYear(for: date2) 125 | return abs(dateComponents([.year], 126 | from: startOfYearForDate1, 127 | to: startOfYearForDate2).year!) 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/Protocols/Calendar+Axis.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 12:43 PM - 8/4/20 2 | 3 | import SwiftUI 4 | 5 | public protocol AxisModifiable: Buildable { 6 | 7 | var axis: Axis { get set } 8 | 9 | } 10 | 11 | extension AxisModifiable { 12 | 13 | /// Sets the axis of the calendar to vertical 14 | public func vertical() -> Self { 15 | axis(.vertical) 16 | } 17 | 18 | /// Sets the axis of the calendar to vertical 19 | public func horizontal() -> Self { 20 | axis(.horizontal) 21 | } 22 | 23 | /// Sets the axis of the calendar 24 | /// 25 | /// - Parameter axis: the intended axis of the calendar 26 | public func axis(_ axis: Axis) -> Self { 27 | mutating(keyPath: \.axis, value: axis) 28 | } 29 | 30 | } 31 | 32 | extension MonthlyCalendarView: AxisModifiable { } 33 | extension YearlyCalendarView: AxisModifiable { } 34 | extension ElegantCalendarView: AxisModifiable { } 35 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/Protocols/Calendar+Buildable.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 5:36 PM - 7/14/20 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | /// Adds a helper function to mutate a properties and help implement _Builder_ pattern 7 | public protocol Buildable { } 8 | 9 | extension Buildable { 10 | 11 | /// Mutates a property of the instance 12 | /// 13 | /// - Parameter keyPath: `WritableKeyPath` to the instance property to be modified 14 | /// - Parameter value: value to overwrite the instance property 15 | func mutating(keyPath: WritableKeyPath, value: T) -> Self { 16 | var newSelf = self 17 | newSelf[keyPath: keyPath] = value 18 | return newSelf 19 | } 20 | 21 | } 22 | 23 | extension MonthlyCalendarView: Buildable { 24 | 25 | /// Changes the theme of the calendar 26 | /// 27 | /// - Parameter theme: theme of various components of the calendar 28 | public func theme(_ theme: CalendarTheme) -> Self { 29 | defer { 30 | calendarManager.listManager.reloadPages() 31 | } 32 | return mutating(keyPath: \.theme, value: theme) 33 | } 34 | 35 | /// Sets whether haptics is enabled or not 36 | /// 37 | /// - Parameter value: `true` if haptics is allowed, `false`, otherwise. Defaults to `true` 38 | public func allowsHaptics(_ value: Bool = true) -> Self { 39 | calendarManager.allowsHaptics = value 40 | return self 41 | } 42 | 43 | } 44 | 45 | extension ElegantCalendarView: Buildable { 46 | 47 | /// Changes the theme of the calendar 48 | /// 49 | /// - Parameter theme: theme of various components of the calendar 50 | public func theme(_ theme: CalendarTheme) -> Self { 51 | mutating(keyPath: \.theme, value: theme) 52 | } 53 | 54 | /// Sets whether haptics is enabled or not 55 | /// 56 | /// - Parameter value: `true` if haptics is allowed, `false`, otherwise. Defaults to `true` 57 | public func allowsHaptics(_ value: Bool = true) -> Self { 58 | calendarManager.monthlyManager.allowsHaptics = value 59 | return self 60 | } 61 | 62 | } 63 | 64 | extension YearlyCalendarView: Buildable { 65 | 66 | /// Changes the theme of the calendar 67 | /// 68 | /// - Parameter theme: theme of various components of the calendar 69 | public func theme(_ theme: CalendarTheme) -> Self { 70 | mutating(keyPath: \.theme, value: theme) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/Protocols/ElegantCalendarCommunicator.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 2:42 PM - 7/12/20 2 | 3 | import Foundation 4 | 5 | public protocol ElegantCalendarCommunicator { 6 | 7 | func scrollToMonthAndShowMonthlyView(_ month: Date) 8 | func showYearlyView() 9 | 10 | } 11 | 12 | public extension ElegantCalendarCommunicator { 13 | 14 | func scrollToMonthAndShowMonthlyView(_ month: Date) { } 15 | func showYearlyView() { } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/Protocols/ElegantCalendarDataSource.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 5:19 PM - 6/14/20 2 | 3 | import SwiftUI 4 | 5 | public protocol ElegantCalendarDataSource: MonthlyCalendarDataSource, YearlyCalendarDataSource { } 6 | 7 | public protocol MonthlyCalendarDataSource { 8 | 9 | func calendar(backgroundColorOpacityForDate date: Date) -> Double 10 | func calendar(canSelectDate date: Date) -> Bool 11 | func calendar(viewForSelectedDate date: Date, dimensions size: CGSize) -> AnyView 12 | 13 | } 14 | 15 | 16 | public extension MonthlyCalendarDataSource { 17 | 18 | func calendar(backgroundColorOpacityForDate date: Date) -> Double { 1 } 19 | 20 | func calendar(canSelectDate date: Date) -> Bool { true } 21 | 22 | func calendar(viewForSelectedDate date: Date, dimensions size: CGSize) -> AnyView { 23 | EmptyView().erased 24 | } 25 | 26 | } 27 | 28 | // TODO: Depending on future design choices, this may need some functions and properties 29 | public protocol YearlyCalendarDataSource { } 30 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Models/Protocols/ElegantCalendarDelegate.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 5:19 PM - 6/14/20 2 | 3 | import SwiftUI 4 | 5 | public protocol ElegantCalendarDelegate: MonthlyCalendarDelegate, YearlyCalendarDelegate { } 6 | 7 | public protocol MonthlyCalendarDelegate { 8 | 9 | func calendar(didSelectDay date: Date) 10 | func calendar(willDisplayMonth date: Date) 11 | 12 | } 13 | 14 | public extension MonthlyCalendarDelegate { 15 | 16 | func calendar(didSelectDay date: Date) { } 17 | func calendar(willDisplayMonth date: Date) { } 18 | 19 | } 20 | 21 | public protocol YearlyCalendarDelegate { 22 | 23 | func calendar(didSelectMonth date: Date) 24 | func calendar(willDisplayYear date: Date) 25 | 26 | } 27 | 28 | public extension YearlyCalendarDelegate { 29 | 30 | func calendar(didSelectMonth date: Date) { } 31 | func calendar(willDisplayYear date: Date) { } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Helpers/Previews/LightDarkThemePreview.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:19 AM - 6/7/20 2 | 3 | import SwiftUI 4 | 5 | struct LightDarkThemePreview: View { 6 | 7 | let preview: Preview 8 | 9 | var body: some View { 10 | Group { 11 | LightThemePreview { 12 | self.preview 13 | } 14 | 15 | DarkThemePreview { 16 | self.preview 17 | } 18 | } 19 | } 20 | 21 | init(@ViewBuilder preview: @escaping () -> Preview) { 22 | self.preview = preview() 23 | } 24 | 25 | } 26 | 27 | struct LightThemePreview: View { 28 | 29 | let preview: Preview 30 | 31 | var body: some View { 32 | preview 33 | .previewLayout(.sizeThatFits) 34 | .colorScheme(.light) 35 | } 36 | 37 | init(@ViewBuilder preview: @escaping () -> Preview) { 38 | self.preview = preview() 39 | } 40 | 41 | } 42 | 43 | struct DarkThemePreview: View { 44 | 45 | let preview: Preview 46 | 47 | var body: some View { 48 | preview 49 | .previewLayout(.sizeThatFits) 50 | .colorScheme(.dark) 51 | .background(Color.black.edgesIgnoringSafeArea(.all)) 52 | } 53 | 54 | init(@ViewBuilder preview: @escaping () -> Preview) { 55 | self.preview = preview() 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Views/ElegantCalendarView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:19 PM - 6/6/20 2 | 3 | import ElegantPages 4 | import SwiftUI 5 | 6 | public struct ElegantCalendarView: View { 7 | 8 | var theme: CalendarTheme = .default 9 | public var axis: Axis = .horizontal 10 | 11 | public let calendarManager: ElegantCalendarManager 12 | 13 | public init(calendarManager: ElegantCalendarManager) { 14 | self.calendarManager = calendarManager 15 | } 16 | 17 | public var body: some View { 18 | content 19 | } 20 | 21 | private var content: some View { 22 | Group { 23 | if axis == .vertical { 24 | ElegantVPages(manager: calendarManager.pagesManager) { 25 | yearlyCalendarView 26 | monthlyCalendarView 27 | } 28 | .onPageChanged(calendarManager.scrollToYearIfOnYearlyView) 29 | .erased 30 | } else { 31 | ElegantHPages(manager: calendarManager.pagesManager) { 32 | yearlyCalendarView 33 | monthlyCalendarView 34 | } 35 | .onPageChanged(calendarManager.scrollToYearIfOnYearlyView) 36 | .erased 37 | } 38 | } 39 | } 40 | 41 | private var yearlyCalendarView: some View { 42 | YearlyCalendarView(calendarManager: calendarManager.yearlyManager) 43 | .axis(axis.inverted) 44 | .theme(theme) 45 | } 46 | 47 | private var monthlyCalendarView: some View { 48 | MonthlyCalendarView(calendarManager: calendarManager.monthlyManager) 49 | .axis(axis.inverted) 50 | .theme(theme) 51 | } 52 | 53 | } 54 | 55 | 56 | struct ElegantCalendarView_Previews: PreviewProvider { 57 | static var previews: some View { 58 | // Only run one calendar at a time. SwiftUI has a limit for rendering time 59 | Group { 60 | 61 | // LightThemePreview { 62 | // ElegantCalendarView(calendarManager: ElegantCalendarManager(configuration: .mock)) 63 | 64 | // ElegantCalendarView(calendarManager: ElegantCalendarManager(configuration: .mock, initialMonth: Date())) 65 | // } 66 | 67 | DarkThemePreview { 68 | ElegantCalendarView(calendarManager: ElegantCalendarManager(configuration: .mock)) 69 | 70 | // ElegantCalendarView(calendarManager: ElegantCalendarManager(configuration: .mock, initialMonth: Date())) 71 | } 72 | 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Views/Monthly/DayView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 11:30 PM - 6/6/20 2 | 3 | import SwiftUI 4 | 5 | struct DayView: View, MonthlyCalendarManagerDirectAccess { 6 | 7 | @Environment(\.calendarTheme) var theme: CalendarTheme 8 | 9 | @ObservedObject var calendarManager: MonthlyCalendarManager 10 | 11 | let week: Date 12 | let day: Date 13 | 14 | private var isDayWithinDateRange: Bool { 15 | day >= calendar.startOfDay(for: startDate) && day <= endDate 16 | } 17 | 18 | private var isDayWithinWeekMonthAndYear: Bool { 19 | calendar.isDate(week, equalTo: day, toGranularities: [.month, .year]) 20 | } 21 | 22 | private var canSelectDay: Bool { 23 | datasource?.calendar(canSelectDate: day) ?? true 24 | } 25 | 26 | private var isDaySelectableAndInRange: Bool { 27 | isDayWithinDateRange && isDayWithinWeekMonthAndYear && canSelectDay 28 | } 29 | 30 | private var isDayToday: Bool { 31 | calendar.isDateInToday(day) 32 | } 33 | 34 | private var isSelected: Bool { 35 | guard let selectedDate = selectedDate else { return false } 36 | return calendar.isDate(selectedDate, equalTo: day, toGranularities: [.day, .month, .year]) 37 | } 38 | 39 | var body: some View { 40 | Text(numericDay) 41 | .font(.footnote) 42 | .foregroundColor(foregroundColor) 43 | .frame(width: CalendarConstants.Monthly.dayWidth, height: CalendarConstants.Monthly.dayWidth) 44 | .background(backgroundColor) 45 | .clipShape(Circle()) 46 | .opacity(opacity) 47 | .overlay(isSelected ? CircularSelectionView() : nil) 48 | .onTapGesture(perform: notifyManager) 49 | } 50 | 51 | private var numericDay: String { 52 | String(calendar.component(.day, from: day)) 53 | } 54 | 55 | private var foregroundColor: Color { 56 | if isDayToday { 57 | return theme.todayTextColor 58 | } else { 59 | return theme.textColor 60 | } 61 | } 62 | 63 | private var backgroundColor: some View { 64 | Group { 65 | if isDayToday { 66 | theme.todayBackgroundColor 67 | } else if isDaySelectableAndInRange { 68 | theme.primary 69 | .opacity(datasource?.calendar(backgroundColorOpacityForDate: day) ?? 1) 70 | } else { 71 | Color.clear 72 | } 73 | } 74 | } 75 | 76 | private var opacity: Double { 77 | guard !isDayToday else { return 1 } 78 | return isDaySelectableAndInRange ? 1 : 0.15 79 | } 80 | 81 | private func notifyManager() { 82 | guard isDayWithinDateRange && canSelectDay else { return } 83 | 84 | if isDayToday || isDayWithinWeekMonthAndYear { 85 | calendarManager.dayTapped(day: day, withHaptic: true) 86 | } 87 | } 88 | 89 | } 90 | 91 | private struct CircularSelectionView: View { 92 | 93 | @State private var startBounce = false 94 | 95 | var body: some View { 96 | Circle() 97 | .stroke(Color.primary, lineWidth: 2) 98 | .frame(width: radius, height: radius) 99 | .opacity(startBounce ? 1 : 0) 100 | .animation(.interpolatingSpring(stiffness: 150, damping: 10)) 101 | .onAppear(perform: startBounceAnimation) 102 | } 103 | 104 | private var radius: CGFloat { 105 | startBounce ? CalendarConstants.Monthly.dayWidth + 6 : CalendarConstants.Monthly.dayWidth + 25 106 | } 107 | 108 | private func startBounceAnimation() { 109 | startBounce = true 110 | } 111 | 112 | } 113 | 114 | struct DayView_Previews: PreviewProvider { 115 | static var previews: some View { 116 | LightDarkThemePreview { 117 | DayView(calendarManager: .mock, week: Date(), day: Date()) 118 | 119 | DayView(calendarManager: .mock, week: Date(), day: .daysFromToday(3)) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Views/Monthly/MonthView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 10:53 PM - 6/6/20 2 | 3 | import SwiftUI 4 | 5 | struct MonthView: View, MonthlyCalendarManagerDirectAccess { 6 | 7 | @Environment(\.calendarTheme) var theme: CalendarTheme 8 | 9 | @ObservedObject var calendarManager: MonthlyCalendarManager 10 | 11 | let month: Date 12 | 13 | private var weeks: [Date] { 14 | guard let monthInterval = calendar.dateInterval(of: .month, for: month) else { 15 | return [] 16 | } 17 | return calendar.generateDates( 18 | inside: monthInterval, 19 | matching: calendar.firstDayOfEveryWeek) 20 | } 21 | 22 | private var isWithinSameMonthAndYearAsToday: Bool { 23 | calendar.isDate(month, equalTo: Date(), toGranularities: [.month, .year]) 24 | } 25 | 26 | var body: some View { 27 | VStack(spacing: 40) { 28 | monthYearHeader 29 | .padding(.leading, CalendarConstants.Monthly.outerHorizontalPadding) 30 | .onTapGesture { self.communicator?.showYearlyView() } 31 | weeksViewWithDaysOfWeekHeader 32 | if selectedDate != nil { 33 | calenderAccessoryView 34 | .padding(.leading, CalendarConstants.Monthly.outerHorizontalPadding) 35 | .id(selectedDate!) 36 | } 37 | Spacer() 38 | } 39 | .padding(.top, CalendarConstants.Monthly.topPadding) 40 | .frame(width: CalendarConstants.Monthly.cellWidth, height: CalendarConstants.cellHeight) 41 | } 42 | 43 | } 44 | 45 | private extension MonthView { 46 | 47 | var monthYearHeader: some View { 48 | HStack { 49 | VStack(alignment: .leading) { 50 | monthText 51 | yearText 52 | } 53 | Spacer() 54 | } 55 | } 56 | 57 | var monthText: some View { 58 | Text(month.fullMonth.uppercased()) 59 | .font(.system(size: 26)) 60 | .bold() 61 | .tracking(7) 62 | .foregroundColor(isWithinSameMonthAndYearAsToday ? theme.titleColor : .primary) 63 | } 64 | 65 | var yearText: some View { 66 | Text(month.year) 67 | .font(.system(size: 12)) 68 | .tracking(2) 69 | .foregroundColor(isWithinSameMonthAndYearAsToday ? theme.titleColor : .gray) 70 | .opacity(0.95) 71 | } 72 | 73 | } 74 | 75 | private extension MonthView { 76 | 77 | var weeksViewWithDaysOfWeekHeader: some View { 78 | VStack(spacing: 32) { 79 | daysOfWeekHeader 80 | weeksViewStack 81 | } 82 | } 83 | 84 | var daysOfWeekHeader: some View { 85 | HStack(spacing: CalendarConstants.Monthly.gridSpacing) { 86 | ForEach(calendar.shortWeekdaySymbols, id: \.self) { dayOfWeek in 87 | Text(dayOfWeek.prefix(1)) 88 | .font(.caption) 89 | .frame(width: CalendarConstants.Monthly.dayWidth) 90 | .foregroundColor(.gray) 91 | } 92 | } 93 | } 94 | 95 | var weeksViewStack: some View { 96 | VStack(spacing: CalendarConstants.Monthly.gridSpacing) { 97 | ForEach(weeks, id: \.self) { week in 98 | WeekView(calendarManager: self.calendarManager, week: week) 99 | } 100 | } 101 | } 102 | 103 | } 104 | 105 | private extension MonthView { 106 | 107 | var calenderAccessoryView: some View { 108 | CalendarAccessoryView(calendarManager: calendarManager) 109 | } 110 | 111 | } 112 | 113 | private struct CalendarAccessoryView: View, MonthlyCalendarManagerDirectAccess { 114 | 115 | let calendarManager: MonthlyCalendarManager 116 | 117 | @State private var isVisible = false 118 | 119 | private var numberOfDaysFromTodayToSelectedDate: Int { 120 | let startOfToday = calendar.startOfDay(for: Date()) 121 | let startOfSelectedDate = calendar.startOfDay(for: selectedDate!) 122 | return calendar.dateComponents([.day], from: startOfToday, to: startOfSelectedDate).day! 123 | } 124 | 125 | private var isNotYesterdayTodayOrTomorrow: Bool { 126 | abs(numberOfDaysFromTodayToSelectedDate) > 1 127 | } 128 | 129 | var body: some View { 130 | VStack { 131 | selectedDayInformationView 132 | GeometryReader { geometry in 133 | self.datasource?.calendar(viewForSelectedDate: self.selectedDate!, 134 | dimensions: geometry.size) 135 | } 136 | } 137 | .onAppear(perform: makeVisible) 138 | .opacity(isVisible ? 1 : 0) 139 | .animation(.easeInOut(duration: 0.5)) 140 | } 141 | 142 | private func makeVisible() { 143 | isVisible = true 144 | } 145 | 146 | private var selectedDayInformationView: some View { 147 | HStack { 148 | VStack(alignment: .leading) { 149 | dayOfWeekWithMonthAndDayText 150 | if isNotYesterdayTodayOrTomorrow { 151 | daysFromTodayText 152 | } 153 | } 154 | Spacer() 155 | } 156 | } 157 | 158 | private var dayOfWeekWithMonthAndDayText: some View { 159 | let monthDayText: String 160 | if numberOfDaysFromTodayToSelectedDate == -1 { 161 | monthDayText = "Yesterday" 162 | } else if numberOfDaysFromTodayToSelectedDate == 0 { 163 | monthDayText = "Today" 164 | } else if numberOfDaysFromTodayToSelectedDate == 1 { 165 | monthDayText = "Tomorrow" 166 | } else { 167 | monthDayText = selectedDate!.dayOfWeekWithMonthAndDay 168 | } 169 | 170 | return Text(monthDayText.uppercased()) 171 | .font(.subheadline) 172 | .bold() 173 | } 174 | 175 | private var daysFromTodayText: some View { 176 | let isBeforeToday = numberOfDaysFromTodayToSelectedDate < 0 177 | let daysDescription = isBeforeToday ? "DAYS AGO" : "DAYS FROM TODAY" 178 | 179 | return Text("\(abs(numberOfDaysFromTodayToSelectedDate)) \(daysDescription)") 180 | .font(.system(size: 10)) 181 | .foregroundColor(.gray) 182 | } 183 | 184 | } 185 | 186 | struct MonthView_Previews: PreviewProvider { 187 | static var previews: some View { 188 | LightDarkThemePreview { 189 | MonthView(calendarManager: .mock, month: Date()) 190 | 191 | MonthView(calendarManager: .mock, month: .daysFromToday(45)) 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Views/Monthly/MonthlyCalendarView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 2:26 PM - 6/14/20 2 | 3 | import ElegantPages 4 | import SwiftUI 5 | 6 | public struct MonthlyCalendarView: View, MonthlyCalendarManagerDirectAccess { 7 | 8 | var theme: CalendarTheme = .default 9 | public var axis: Axis = .vertical 10 | 11 | @ObservedObject public var calendarManager: MonthlyCalendarManager 12 | 13 | private var isTodayWithinDateRange: Bool { 14 | Date() >= calendar.startOfDay(for: startDate) && 15 | calendar.startOfDay(for: Date()) <= endDate 16 | } 17 | 18 | private var isCurrentMonthYearSameAsTodayMonthYear: Bool { 19 | calendar.isDate(currentMonth, equalTo: Date(), toGranularities: [.month, .year]) 20 | } 21 | 22 | public init(calendarManager: MonthlyCalendarManager) { 23 | self.calendarManager = calendarManager 24 | } 25 | 26 | public var body: some View { 27 | GeometryReader { geometry in 28 | self.content(geometry: geometry) 29 | } 30 | } 31 | 32 | private func content(geometry: GeometryProxy) -> some View { 33 | CalendarConstants.Monthly.cellWidth = geometry.size.width 34 | 35 | return ZStack(alignment: .top) { 36 | monthsList 37 | 38 | if isTodayWithinDateRange && !isCurrentMonthYearSameAsTodayMonthYear { 39 | leftAlignedScrollBackToTodayButton 40 | .padding(.trailing, CalendarConstants.Monthly.outerHorizontalPadding) 41 | .offset(y: CalendarConstants.Monthly.topPadding + 3) 42 | .transition(.opacity) 43 | } 44 | } 45 | .frame(height: CalendarConstants.cellHeight) 46 | } 47 | 48 | private var monthsList: some View { 49 | Group { 50 | if axis == .vertical { 51 | ElegantVList(manager: listManager, 52 | pageTurnType: .monthlyEarlyCutoff, 53 | viewForPage: monthView) 54 | .onPageChanged(configureNewMonth) 55 | .frame(width: CalendarConstants.Monthly.cellWidth) 56 | } else { 57 | ElegantHList(manager: listManager, 58 | pageTurnType: .monthlyEarlyCutoff, 59 | viewForPage: monthView) 60 | .onPageChanged(configureNewMonth) 61 | .frame(width: CalendarConstants.Monthly.cellWidth) 62 | } 63 | } 64 | } 65 | 66 | private func monthView(for page: Int) -> AnyView { 67 | MonthView(calendarManager: calendarManager, month: months[page]) 68 | .environment(\.calendarTheme, theme) 69 | .erased 70 | } 71 | 72 | private var leftAlignedScrollBackToTodayButton: some View { 73 | HStack { 74 | Spacer() 75 | ScrollBackToTodayButton(scrollBackToToday: scrollBackToToday, 76 | color: theme.primary) 77 | } 78 | } 79 | 80 | } 81 | 82 | struct MonthlyCalendarView_Previews: PreviewProvider { 83 | static var previews: some View { 84 | LightDarkThemePreview { 85 | MonthlyCalendarView(calendarManager: .mock) 86 | 87 | MonthlyCalendarView(calendarManager: .mockWithInitialMonth) 88 | } 89 | } 90 | } 91 | 92 | private extension PageTurnType { 93 | 94 | static var monthlyEarlyCutoff: PageTurnType = .earlyCutoff(config: .monthlyConfig) 95 | 96 | } 97 | 98 | public extension EarlyCutOffConfiguration { 99 | 100 | static let monthlyConfig = EarlyCutOffConfiguration( 101 | scrollResistanceCutOff: 40, 102 | pageTurnCutOff: 80, 103 | pageTurnAnimation: .spring(response: 0.3, dampingFraction: 0.95)) 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Views/Monthly/WeekView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 10:54 PM - 6/6/20 2 | 3 | import SwiftUI 4 | 5 | struct WeekView: View, MonthlyCalendarManagerDirectAccess { 6 | 7 | let calendarManager: MonthlyCalendarManager 8 | 9 | let week: Date 10 | 11 | private var days: [Date] { 12 | guard let weekInterval = calendar.dateInterval(of: .weekOfYear, for: week) else { 13 | return [] 14 | } 15 | return calendar.generateDates( 16 | inside: weekInterval, 17 | matching: .everyDay) 18 | } 19 | 20 | var body: some View { 21 | HStack(spacing: CalendarConstants.Monthly.gridSpacing) { 22 | ForEach(days, id: \.self) { day in 23 | DayView(calendarManager: self.calendarManager, week: self.week, day: day) 24 | } 25 | } 26 | } 27 | 28 | } 29 | 30 | struct WeekView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | LightDarkThemePreview { 33 | WeekView(calendarManager: .mock, week: Date()) 34 | 35 | WeekView(calendarManager: .mock, week: .daysFromToday(-7)) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Views/Shared/ScrollBackToTodayButton.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:14 PM - 6/14/20 2 | 3 | import SwiftUI 4 | 5 | struct ScrollBackToTodayButton: View { 6 | 7 | let scrollBackToToday: () -> Void 8 | let color: Color 9 | 10 | var body: some View { 11 | Button(action: scrollBackToToday) { 12 | Image.uTurnLeft 13 | .resizable() 14 | .frame(width: 30, height: 25) 15 | .foregroundColor(color) 16 | } 17 | .animation(.easeInOut) 18 | } 19 | 20 | } 21 | 22 | struct ScrollBackToTodayButton_Previews: PreviewProvider { 23 | static var previews: some View { 24 | LightDarkThemePreview { 25 | ScrollBackToTodayButton(scrollBackToToday: {}, color: .purple) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Views/Yearly/SmallDayView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:19 PM - 6/13/20 2 | 3 | import SwiftUI 4 | 5 | struct SmallDayView: View, YearlyCalendarManagerDirectAccess { 6 | 7 | let calendarManager: YearlyCalendarManager 8 | 9 | let week: Date 10 | let day: Date 11 | 12 | private var isDayWithinDateRange: Bool { 13 | day >= calendar.startOfDay(for: startDate) && day <= endDate 14 | } 15 | 16 | private var isDayWithinWeekMonthAndYear: Bool { 17 | calendar.isDate(week, equalTo: day, toGranularities: [.month, .year]) 18 | } 19 | 20 | private var isDayToday: Bool { 21 | calendar.isDateInToday(day) 22 | } 23 | 24 | var body: some View { 25 | Text(numericDay) 26 | .font(.system(size: 8)) 27 | .foregroundColor(isDayToday ? .systemBackground : .primary) 28 | .frame(width: CalendarConstants.Yearly.dayWidth, height: CalendarConstants.Yearly.dayWidth) 29 | .background(isDayToday ? Circle().fill(Color.primary) : nil) 30 | .opacity(isDayWithinDateRange && isDayWithinWeekMonthAndYear ? 1 : 0) 31 | } 32 | 33 | private var numericDay: String { 34 | String(calendar.component(.day, from: day)) 35 | } 36 | 37 | } 38 | 39 | struct SmallDayView_Previews: PreviewProvider { 40 | static var previews: some View { 41 | LightDarkThemePreview { 42 | SmallDayView(calendarManager: .mock, week: Date(), day: Date()) 43 | 44 | SmallDayView(calendarManager: .mock, week: Date(), day: .daysFromToday(3)) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Views/Yearly/SmallMonthView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:16 PM - 6/13/20 2 | 3 | import SwiftUI 4 | 5 | struct SmallMonthView: View, YearlyCalendarManagerDirectAccess { 6 | 7 | @Environment(\.calendarTheme) var theme: CalendarTheme 8 | 9 | let calendarManager: YearlyCalendarManager 10 | 11 | let month: Date 12 | 13 | private var weeks: [Date] { 14 | guard let monthInterval = calendar.dateInterval(of: .month, for: month) else { 15 | return [] 16 | } 17 | return calendar.generateDates( 18 | inside: monthInterval, 19 | matching: calendar.firstDayOfEveryWeek) 20 | } 21 | 22 | private var isWithinSameMonthAndYearAsToday: Bool { 23 | calendar.isDate(month, equalTo: Date(), toGranularities: [.month, .year]) 24 | } 25 | 26 | private var isWithinDateRange: Bool { 27 | let startOfMonth = calendar.date(from: calendar.dateComponents([.month, .year], from: month))! 28 | let startOfStartDate = calendar.date(from: calendar.dateComponents([.month, .year], from: startDate))! 29 | let startOfEndDate = calendar.date(from: calendar.dateComponents([.month, .year], from: endDate))! 30 | return startOfMonth >= startOfStartDate && startOfMonth <= startOfEndDate 31 | } 32 | 33 | var body: some View { 34 | VStack(alignment: .leading, spacing: 4) { 35 | monthText 36 | weeksViewStack 37 | .frame(height: CalendarConstants.Yearly.daysStackHeight) 38 | } 39 | .frame(width: CalendarConstants.Yearly.monthWidth) 40 | .contentShape(Rectangle()) 41 | .opacity(isWithinDateRange ? 1 : 0) 42 | .onTapGesture(perform: currentMonthSelected) 43 | } 44 | 45 | private var monthText: some View { 46 | Text(month.abbreviatedMonth.uppercased()) 47 | .font(.subheadline) 48 | .bold() 49 | .foregroundColor(isWithinSameMonthAndYearAsToday ? theme.primary : .primary) 50 | } 51 | 52 | private var weeksViewStack: some View { 53 | VStack(spacing: CalendarConstants.Yearly.daysGridVerticalSpacing) { 54 | ForEach(weeks, id: \.self) { week in 55 | SmallWeekView(calendarManager: self.calendarManager, week: week) 56 | } 57 | if weeks.count == 5 { 58 | Spacer() 59 | } 60 | } 61 | } 62 | 63 | private func currentMonthSelected() { 64 | calendarManager.monthTapped(month) 65 | } 66 | 67 | } 68 | 69 | struct SmallMonthView_Previews: PreviewProvider { 70 | static var previews: some View { 71 | LightDarkThemePreview { 72 | SmallMonthView(calendarManager: .mock, month: Date()) 73 | 74 | SmallMonthView(calendarManager: .mock, month: .daysFromToday(45)) 75 | 76 | SmallMonthView(calendarManager: .mock, month: .daysFromToday(-30)) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Views/Yearly/SmallWeekView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 7:18 PM - 6/13/20 2 | 3 | import SwiftUI 4 | 5 | struct SmallWeekView: View, YearlyCalendarManagerDirectAccess { 6 | 7 | let calendarManager: YearlyCalendarManager 8 | 9 | let week: Date 10 | 11 | private var days: [Date] { 12 | guard let weekInterval = calendar.dateInterval(of: .weekOfYear, for: week) else { 13 | return [] 14 | } 15 | return calendar.generateDates( 16 | inside: weekInterval, 17 | matching: .everyDay) 18 | } 19 | 20 | var body: some View { 21 | HStack(spacing: CalendarConstants.Yearly.daysGridHorizontalSpacing) { 22 | ForEach(days, id: \.self) { day in 23 | SmallDayView(calendarManager: self.calendarManager, week: self.week, day: day) 24 | } 25 | } 26 | } 27 | 28 | } 29 | 30 | struct SmallWeekView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | LightDarkThemePreview { 33 | SmallWeekView(calendarManager: .mock, week: Date()) 34 | 35 | SmallWeekView(calendarManager: .mock, week: .daysFromToday(-7)) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ElegantCalendar/Views/Yearly/YearView.swift: -------------------------------------------------------------------------------- 1 | // Kevin Li - 6:56 PM - 6/13/20 2 | 3 | import SwiftUI 4 | 5 | struct YearView: View, YearlyCalendarManagerDirectAccess { 6 | 7 | @Environment(\.calendarTheme) var theme: CalendarTheme 8 | 9 | let calendarManager: YearlyCalendarManager 10 | 11 | let year: Date 12 | 13 | private var isYearSameAsTodayYear: Bool { 14 | calendar.isDate(year, equalTo: Date(), toGranularities: [.year]) 15 | } 16 | 17 | var body: some View { 18 | VStack(alignment: .leading, spacing: 40) { 19 | yearText 20 | monthsStack 21 | Spacer() 22 | } 23 | .padding(.top, CalendarConstants.Yearly.topPadding) 24 | .frame(width: CalendarConstants.Yearly.cellWidth, height: CalendarConstants.cellHeight) 25 | } 26 | 27 | private var yearText: some View { 28 | Text(year.year) 29 | .font(.system(size: 38, weight: .thin, design: .rounded)) 30 | .foregroundColor(isYearSameAsTodayYear ? theme.primary : .primary) 31 | } 32 | 33 | private var monthsStack: some View { 34 | let months: [Date] 35 | if let yearInterval = calendar.dateInterval(of: .year, for: year) { 36 | months = calendar.generateDates( 37 | inside: yearInterval, 38 | matching: .firstDayOfEveryMonth) 39 | } else { 40 | months = [] 41 | } 42 | 43 | return VStack(spacing: CalendarConstants.Yearly.monthsGridSpacing) { 44 | ForEach(0..= calendar.startOfDay(for: startDate) && 14 | calendar.startOfDay(for: Date()) <= endDate 15 | } 16 | 17 | private var isCurrentYearSameAsTodayYear: Bool { 18 | calendar.isDate(currentYear, equalTo: Date(), toGranularities: [.year]) 19 | } 20 | 21 | public init(calendarManager: YearlyCalendarManager) { 22 | self.calendarManager = calendarManager 23 | } 24 | 25 | public var body: some View { 26 | ZStack(alignment: .topTrailing) { 27 | yearsList 28 | .zIndex(0) 29 | if isTodayWithinDateRange && !isCurrentYearSameAsTodayYear { 30 | scrollBackToTodayButton 31 | .padding(.trailing, CalendarConstants.Yearly.outerHorizontalPadding) 32 | .offset(y: CalendarConstants.Yearly.topPadding + 10) 33 | .transition(.opacity) 34 | .zIndex(1) 35 | } 36 | } 37 | } 38 | 39 | private var yearsList: some View { 40 | YearlyCalendarScrollView(axis, calendarManager: calendarManager) { 41 | self.yearsStack 42 | } 43 | .frame(width: CalendarConstants.Yearly.cellWidth, 44 | height: CalendarConstants.cellHeight) 45 | } 46 | 47 | private var yearsStack: some View { 48 | Group { 49 | if axis == .vertical { 50 | VStack(spacing: 0) { 51 | calendarContent 52 | } 53 | } else { 54 | HStack(spacing: 0) { 55 | calendarContent 56 | } 57 | } 58 | } 59 | } 60 | 61 | private var calendarContent: some View { 62 | ForEach(self.years, id: \.self) { year in 63 | YearView(calendarManager: self.calendarManager, year: year) 64 | .environment(\.calendarTheme, self.theme) 65 | } 66 | } 67 | 68 | private var scrollBackToTodayButton: some View { 69 | ScrollBackToTodayButton(scrollBackToToday: calendarManager.scrollBackToToday, 70 | color: theme.primary) 71 | } 72 | 73 | } 74 | 75 | private struct YearlyCalendarScrollView: UIViewControllerRepresentable { 76 | 77 | typealias UIViewControllerType = UIScrollViewViewController 78 | 79 | @ObservedObject var calendarManager: YearlyCalendarManager 80 | 81 | let axis: Axis 82 | let content: AnyView 83 | 84 | var pageLength: CGFloat { 85 | (axis == .vertical) ? CalendarConstants.cellHeight : CalendarConstants.Yearly.cellWidth 86 | } 87 | 88 | var destinationOffset: CGFloat { 89 | pageLength * CGFloat(calendarManager.currentPage.index) 90 | } 91 | 92 | init(_ axis: Axis, calendarManager: YearlyCalendarManager, @ViewBuilder content: @escaping () -> Content) { 93 | self.axis = axis 94 | self.calendarManager = calendarManager 95 | self.content = AnyView(content()) 96 | } 97 | 98 | func makeCoordinator() -> Coordinator { 99 | Coordinator(parent: self) 100 | } 101 | 102 | func makeUIViewController(context: Context) -> UIScrollViewViewController { 103 | UIScrollViewViewController(axis, content: content, delegate: context.coordinator) 104 | } 105 | 106 | func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) { 107 | viewController.hosting.rootView = content 108 | 109 | switch calendarManager.currentPage.state { 110 | case .scroll: 111 | let destinationPoint: CGPoint 112 | if axis == .vertical { 113 | destinationPoint = CGPoint(x: 0, y: destinationOffset) 114 | } else { 115 | destinationPoint = CGPoint(x: destinationOffset, y: 0) 116 | } 117 | 118 | DispatchQueue.main.async { 119 | viewController.scrollView.setContentOffset(destinationPoint, 120 | animated: true) 121 | } 122 | case .completed: 123 | () 124 | } 125 | } 126 | 127 | class Coordinator: NSObject, UIScrollViewDelegate { 128 | 129 | let parent: YearlyCalendarScrollView 130 | 131 | private var calendarManager: YearlyCalendarManager { 132 | parent.calendarManager 133 | } 134 | 135 | init(parent: YearlyCalendarScrollView) { 136 | self.parent = parent 137 | } 138 | 139 | // Called after the user manually drags from one page to another 140 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 141 | let contentOffset = (parent.axis == .vertical) ? scrollView.contentOffset.y : scrollView.contentOffset.x 142 | let page = Int(contentOffset / parent.pageLength) 143 | calendarManager.willDisplay(page: page) 144 | } 145 | 146 | // Called after the `scrollToToday` or any `scrollToYear` animation finishes 147 | func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { 148 | if calendarManager.currentPage.state == .scroll { 149 | let page = Int(parent.destinationOffset / parent.pageLength) 150 | calendarManager.willDisplay(page: page) 151 | } 152 | } 153 | 154 | } 155 | 156 | } 157 | 158 | private class UIScrollViewViewController: UIViewController { 159 | 160 | let hosting: UIHostingController 161 | let scrollView: UIScrollView 162 | 163 | init(_ axis: Axis, content: AnyView, delegate: UIScrollViewDelegate) { 164 | hosting = UIHostingController(rootView: content) 165 | scrollView = UIScrollView().withPagination(delegate: delegate) 166 | super.init(nibName: nil, bundle: nil) 167 | 168 | let fittingSize: CGSize 169 | if axis == .vertical { 170 | fittingSize = CGSize(width: screen.width, height: .greatestFiniteMagnitude) 171 | } else { 172 | fittingSize = CGSize(width: .greatestFiniteMagnitude, height: screen.height) 173 | } 174 | 175 | let size = hosting.view.sizeThatFits(fittingSize) 176 | hosting.view.frame = CGRect(x: 0, y: 0, 177 | width: size.width, 178 | height: size.height) 179 | 180 | scrollView.addSubview(hosting.view) 181 | scrollView.contentSize = CGSize(width: size.width, height: size.height) 182 | } 183 | 184 | required init?(coder: NSCoder) { 185 | fatalError("init(coder:) has not been implemented") 186 | } 187 | 188 | override func viewDidLoad() { 189 | super.viewDidLoad() 190 | 191 | view.addSubview(scrollView) 192 | pinEdges(of: scrollView, to: view) 193 | 194 | hosting.willMove(toParent: self) 195 | scrollView.addSubview(hosting.view) 196 | pinEdges(of: hosting.view, to: scrollView) 197 | hosting.didMove(toParent: self) 198 | } 199 | 200 | func pinEdges(of viewA: UIView, to viewB: UIView) { 201 | viewA.translatesAutoresizingMaskIntoConstraints = false 202 | viewB.addConstraints([ 203 | viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor), 204 | viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor), 205 | viewA.topAnchor.constraint(equalTo: viewB.topAnchor), 206 | viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor), 207 | ]) 208 | } 209 | 210 | } 211 | 212 | 213 | private extension UIScrollView { 214 | 215 | func withPagination(delegate: UIScrollViewDelegate) -> UIScrollView { 216 | backgroundColor = .none 217 | scrollsToTop = false 218 | bounces = false 219 | 220 | contentInsetAdjustmentBehavior = .never 221 | 222 | isPagingEnabled = true 223 | decelerationRate = .fast 224 | 225 | self.delegate = delegate 226 | 227 | return self 228 | } 229 | 230 | } 231 | 232 | struct YearlyCalendarView_Previews: PreviewProvider { 233 | static var previews: some View { 234 | // Only run one calendar at a time. SwiftUI has a limit for rendering time 235 | Group { 236 | 237 | // LightThemePreview { 238 | // YearlyCalendarView(calendarManager: .mock) 239 | // 240 | // YearlyCalendarView(calendarManager: .mockWithInitialYear) 241 | // } 242 | 243 | DarkThemePreview { 244 | // YearlyCalendarView(calendarManager: .mock) 245 | 246 | YearlyCalendarView(calendarManager: .mockWithInitialYear) 247 | } 248 | 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /resources.txt: -------------------------------------------------------------------------------- 1 | https://swiftwithmajid.com/2020/05/06/building-calendar-without-uicollectionview-in-swiftui/ 2 | https://stackoverflow.com/questions/16088615/want-uitableview-to-snap-to-cell 3 | https://stackoverflow.com/a/59963653/6074750 4 | https://stackoverflow.com/questions/27624708/prevent-uitableview-scrolling-below-a-certain-point 5 | https://stackoverflow.com/questions/7797693/force-uitableview-to-scroll-to-tops-of-cells/8082084#8082084 6 | https://stackoverflow.com/questions/29684511/how-to-implement-snap-to-cell-on-a-uitableview-using-swift 7 | https://stackoverflow.com/questions/27722594/ios-jump-to-next-cell-in-uitableview-when-cell-scrolled 8 | https://stackoverflow.com/questions/16284392/uitableview-w-paging-momentum 9 | https://stackoverflow.com/questions/9367600/custom-uiscrollview-paging-with-scrollviewwillenddragging 10 | https://stackoverflow.com/questions/2335249/uitableview-page-size-when-paging-enabled 11 | https://stackoverflow.com/questions/38358112/multiple-delegates-for-uitableview-in-swift 12 | https://stackoverflow.com/questions/29298091/infinitely-scrolling-in-both-directions-of-uicollectionview-with-sections 13 | https://www.objc.io/issues/3-views/scroll-view/ 14 | https://stackoverflow.com/questions/56545444/how-to-remove-highlight-on-tap-of-list-with-swiftui 15 | https://stackoverflow.com/a/10055966/6074750 16 | https://stackoverflow.com/a/18935526/6074750 17 | https://stackoverflow.com/a/49515500/6074750 18 | https://ios-resolution.com/ 19 | https://stackoverflow.com/questions/52652956/how-to-detect-if-the-device-iphone-has-physical-home-button 20 | https://medium.com/swlh/implement-infinite-scroll-with-swiftui-cc25d1459878 21 | https://stackoverflow.com/questions/58406287/how-to-tell-swiftui-views-to-bind-to-nested-observableobjects?noredirect=1&lq=1 22 | https://stackoverflow.com/questions/57730074/transition-animation-not-working-in-swiftui 23 | https://stackoverflow.com/questions/33424684/uiscrollview-paging-with-animation-completion-handler 24 | https://stackoverflow.com/questions/58807357/detect-draggesture-cancelation-in-swiftui 25 | https://github.com/izakpavel/SwiftUIPagingScrollView 26 | https://github.com/warrenburton/DequeueOrNot 27 | https://stackoverflow.com/a/61222819/6074750 28 | https://developer.apple.com/forums/thread/124671 29 | https://stackoverflow.com/questions/58635048/in-a-uiviewcontrollerrepresentable-how-can-i-pass-an-observedobjects-value-to 30 | https://stackoverflow.com/questions/59503399/how-to-define-a-protocol-as-a-type-for-a-observedobject-property 31 | https://www.objc.io/issues/3-views/scroll-view/ 32 | https://stackoverflow.com/questions/36110620/standard-way-to-clamp-a-number-between-two-values-in-swift 33 | https://stackoverflow.com/questions/59211113/gesture-on-a-view-inside-a-scrollview-blocks-the-scrolling 34 | https://stackoverflow.com/questions/57700396/adding-a-drag-gesture-in-swiftui-to-a-view-inside-a-scrollview-blocks-the-scroll?noredirect=1&lq=1 35 | https://stackoverflow.com/questions/61916271/ambiguous-type-name-coordinator-in-myviewcontroller --------------------------------------------------------------------------------