├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── ReadMeResources
├── Gif_ClassicCalendarWithEvents_Scroll.gif
├── Gif_ClassicCalendar_Scroll.gif
├── Gif_ClassicNumbered_Scroll.gif
├── Gif_ClassicNumbered_SnapToToday.gif
├── Gif_CustomCalendar_Scroll.gif
├── Integration
│ ├── SPM_Instructions_1.png
│ └── SPM_Instructions_2.png
├── MooCalLogo.png
├── SS_ClassicCalendarData.png
├── Screenshot 2024-01-14 at 1.05.05 PM.png
├── TestApp
│ ├── Gif_TestApp_1.gif
│ ├── Gif_TestApp_2.gif
│ ├── Gif_TestApp_3.gif
│ └── xcodeproj_location.png
├── WikiResources
│ ├── CustoizingDOWIndicatorView
│ │ ├── customDOWIndicator.png
│ │ ├── dowIndicatorAbbreviated.png
│ │ └── highlighDOWIndicator.png
│ └── CustomizingHeaderView
│ │ ├── CustomHeader.png
│ │ └── DefaultHeader.png
└── labeledCustomizableViews.png
├── Sources
├── MooCal
│ ├── DataModels
│ │ ├── CalendarDay.swift
│ │ └── CalendarMonth.swift
│ ├── Enums
│ │ ├── CalendarDayOfWeekIndicatorView.swift
│ │ ├── CalendarDayStyle.swift
│ │ ├── CalendarDayView.swift
│ │ ├── CalendarMonthHeaderView.swift
│ │ └── DayOfWeek.swift
│ ├── Extensions
│ │ └── Date+Ext.swift
│ ├── MooCal.swift
│ ├── Protocols
│ │ └── CalendarData.swift
│ └── Views
│ │ ├── DayViews
│ │ └── ClassicCalendarDayView.swift
│ │ └── TimeViews
│ │ ├── CalendarMonthView.swift
│ │ ├── CalendarYearView.swift
│ │ └── ScrollableCalendarView.swift
└── MooCalTestApp
│ ├── MooCalTestApp.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── MooCalTestApp
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── ContentView.swift
│ ├── ContentViewFour.swift
│ ├── ContentViewThree.swift
│ ├── ContentViewTwo.swift
│ ├── DataModels
│ └── Event.swift
│ ├── MooCalTestAppApp.swift
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ └── Views
│ ├── DayEventView.swift
│ └── EventInputSheet.swift
└── Tests
└── MooCalTests
└── MooCalTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "MooCal",
8 | platforms: [
9 | .iOS(.v16),
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, making them visible to other packages.
13 | .library(
14 | name: "MooCal",
15 | targets: ["MooCal"]),
16 | ],
17 | targets: [
18 | // Targets are the basic building blocks of a package, defining a module or a test suite.
19 | // Targets can depend on other targets in this package and products from dependencies.
20 | .target(
21 | name: "MooCal"),
22 | .testTarget(
23 | name: "MooCalTests",
24 | dependencies: ["MooCal"]),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Moocal is a lightweight SwiftUI calendar library designed to simplify the creation and customization of calendars in your SwiftUI applications. Whether you're building a productivity app, an event planner, or any application that requires calendar functionality, Moocal is your go-to solution. If you want to see an example app using MooCal, see our test app.
11 |
12 | [Access Test App](https://github.com/mazefest/MooCal/wiki/Accessing-the-Test-App)
13 |
14 | # Requirements
15 |
16 | | Platforms | Minimum Swift Version |
17 | |--------------|-----------------------|
18 | | iOS 16+ | 5.9 |
19 |
20 | # Getting Started
21 |
22 | ### Swift Package Manager
23 | 1. in XCode go to `File` -> `Add Package Dependencies...`
24 |
25 |
26 |
27 | 2. In the searchbar paste the github url `https://github.com/mazefest/MooCal` and select `Add Package`.
28 |
29 |
30 |
31 | Now you can import MooCal and use the library where ever you like.
32 |
33 | # Integration
34 |
35 | ### 1. Import MooCal
36 |
37 | Import MooCal inside of the desired file you are wanting to implement MooCal's features.
38 |
39 | ```swift
40 | import MooCal
41 | ````
42 |
43 | ### 2. Setup View
44 |
45 | In the view you are wanting to add the scrollable calendar, create a `ScrollableCalendarViewViewModel` and store it as a variable.
46 |
47 | ```swift
48 | struct ContentView: View {
49 | var viewModel: ScrollableCalendarViewViewModel
50 |
51 | init() {
52 | self.viewModel = ScrollableCalendarViewViewModel()
53 | }
54 |
55 | var body: some View {
56 |
57 | }
58 | }
59 | ```
60 |
61 | Next you will need to initialize the `ScrollableCalendarView` inside of the var body, it takes in two parameters.
62 |
63 |
64 | ```swift
65 | ScrollableCalendarView(viewModel: ScrollableCalendarViewViewModel, calendarDayView: CalendarDayView)
66 | ```
67 |
68 | * `viewModel` - [ScrollableCalendarViewViewModel] The viewModel we just made above
69 | * `calendaryDayView` - [CalendarDayView] This determines the view of each individual day inside the calendar
70 |
71 | ## 3. Select Calendar Style
72 | In this documentation we are going ot continue on with the `Custom` calendar day view implementation, but if you are interested in the other offering, checkout the our premade calendar day view implementations below by linking to our wiki.
73 |
74 | 1. Custom
75 | 2. [Classic](https://github.com/mazefest/MooCal/wiki/Default-Calendar-Styles#classic-implementation)
76 | 3. [Numbered](https://github.com/mazefest/MooCal/wiki/Default-Calendar-Styles#numbered-implementation)
77 |
78 | ## 4. Implementing Calendar Style (Custom Implementation)
79 |
80 | When choosing to use the custom implementation your code will look like the following. The Custom implementation provides a completion handler where you will return the `CalendarDay` of the view you need to draw, which gives you all you need to create your custom calendar day view, and then you will have to return the `View`.
81 |
82 | ```swift
83 | var body: some View {
84 | ScrollableCalendarView(
85 | viewModel: viewModel,
86 | calendarDayView: .custom( // <--- HERE
87 | { calendarDay in
88 | // Your custom view implementation here
89 | }
90 | )
91 | )
92 | }
93 | ```
94 |
95 | Now you will need to provide your custom view to the completion handle. The `CalendarDay` is going to be vital for doing so. I recommend creating a function that takes in a `CalendarDay` parameter and returns a view. Forexample:
96 |
97 | ```swift
98 | private func customCalendarDayView(_ calendarDay: CalendarDay) -> some View {
99 | ZStack {
100 | RoundedRectangle(cornerRadius: 10.0)
101 | .foregroundStyle(Color.gray.opacity(0.13))
102 | Text(calendarDay.descriptor) // The day number. ex: 11
103 | .bold()
104 | }
105 | .aspectRatio(contentMode: .fit)
106 | }
107 | ```
108 |
109 | This function will return a rounded rectangle with the numbered day of month centered on top of it.
110 |
111 | Next you will need to put this function inside the compltion handler.
112 |
113 | ```swift
114 | var body: some View {
115 | ScrollableCalendarView(
116 | viewModel: viewModel,
117 | calendarDayView: .custom(
118 | { calendarDay in
119 | customCalendarDayView(calendarDay) // <-- HERE
120 | }
121 | )
122 | )
123 | }
124 |
125 | ```
126 |
127 | This will be the result of the above code.
128 |
129 |
130 |
131 | # Further Customization
132 | If you want to have full control on the stlying of your calendar, checkout the below ways to style the calendar to your liking.
133 |
134 |
135 |
136 | * [Customizing Month Header View](https://github.com/mazefest/MooCal/wiki/Customization#customizing-month-header-view)
137 | * [Customizing the Days Of Week Indicators](https://github.com/mazefest/MooCal/wiki/Customization#customizing-the-days-of-week-indicators)
138 | * [Customizing the Calendar Day](https://github.com/mazefest/MooCal/wiki/Customization#customizing-calendar-days)
139 |
140 |
141 | # Further Implementation
142 | If you want to add more functionality to your calendar, make sure to take a look at our Wiki, there is a lot more functionality you can add very easily.
143 |
144 | * [Responding To Day Selection](https://github.com/mazefest/MooCal/wiki/Further-Implementation#responding-to-day-selection)
145 | * [Auto Scrolling To Current Month](https://github.com/mazefest/MooCal/wiki/Further-Implementation#auto-scrolling-to-current-month)
146 |
147 | # Test App
148 | For full examples of implementing a full calendar app, see the test app below.
149 |
150 | [Access Test App](https://github.com/mazefest/MooCal/wiki/Accessing-the-Test-App)
151 |
--------------------------------------------------------------------------------
/ReadMeResources/Gif_ClassicCalendarWithEvents_Scroll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/Gif_ClassicCalendarWithEvents_Scroll.gif
--------------------------------------------------------------------------------
/ReadMeResources/Gif_ClassicCalendar_Scroll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/Gif_ClassicCalendar_Scroll.gif
--------------------------------------------------------------------------------
/ReadMeResources/Gif_ClassicNumbered_Scroll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/Gif_ClassicNumbered_Scroll.gif
--------------------------------------------------------------------------------
/ReadMeResources/Gif_ClassicNumbered_SnapToToday.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/Gif_ClassicNumbered_SnapToToday.gif
--------------------------------------------------------------------------------
/ReadMeResources/Gif_CustomCalendar_Scroll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/Gif_CustomCalendar_Scroll.gif
--------------------------------------------------------------------------------
/ReadMeResources/Integration/SPM_Instructions_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/Integration/SPM_Instructions_1.png
--------------------------------------------------------------------------------
/ReadMeResources/Integration/SPM_Instructions_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/Integration/SPM_Instructions_2.png
--------------------------------------------------------------------------------
/ReadMeResources/MooCalLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/MooCalLogo.png
--------------------------------------------------------------------------------
/ReadMeResources/SS_ClassicCalendarData.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/SS_ClassicCalendarData.png
--------------------------------------------------------------------------------
/ReadMeResources/Screenshot 2024-01-14 at 1.05.05 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/Screenshot 2024-01-14 at 1.05.05 PM.png
--------------------------------------------------------------------------------
/ReadMeResources/TestApp/Gif_TestApp_1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/TestApp/Gif_TestApp_1.gif
--------------------------------------------------------------------------------
/ReadMeResources/TestApp/Gif_TestApp_2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/TestApp/Gif_TestApp_2.gif
--------------------------------------------------------------------------------
/ReadMeResources/TestApp/Gif_TestApp_3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/TestApp/Gif_TestApp_3.gif
--------------------------------------------------------------------------------
/ReadMeResources/TestApp/xcodeproj_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/TestApp/xcodeproj_location.png
--------------------------------------------------------------------------------
/ReadMeResources/WikiResources/CustoizingDOWIndicatorView/customDOWIndicator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/WikiResources/CustoizingDOWIndicatorView/customDOWIndicator.png
--------------------------------------------------------------------------------
/ReadMeResources/WikiResources/CustoizingDOWIndicatorView/dowIndicatorAbbreviated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/WikiResources/CustoizingDOWIndicatorView/dowIndicatorAbbreviated.png
--------------------------------------------------------------------------------
/ReadMeResources/WikiResources/CustoizingDOWIndicatorView/highlighDOWIndicator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/WikiResources/CustoizingDOWIndicatorView/highlighDOWIndicator.png
--------------------------------------------------------------------------------
/ReadMeResources/WikiResources/CustomizingHeaderView/CustomHeader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/WikiResources/CustomizingHeaderView/CustomHeader.png
--------------------------------------------------------------------------------
/ReadMeResources/WikiResources/CustomizingHeaderView/DefaultHeader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/WikiResources/CustomizingHeaderView/DefaultHeader.png
--------------------------------------------------------------------------------
/ReadMeResources/labeledCustomizableViews.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mazefest/MooCal/009496e5c12a4f7d9028b7bdc1546ad61fe0a990/ReadMeResources/labeledCustomizableViews.png
--------------------------------------------------------------------------------
/Sources/MooCal/DataModels/CalendarDay.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CalendarDay.swift
3 | // CalyKitTestApp
4 | //
5 | // Created by Colby Mehmen on 10/19/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | public protocol ClassicCalendarData: Identifiable, Hashable {
12 | var id: UUID { get set }
13 | var date: Date { get set }
14 | var color: Color { get set }
15 | var title: String { get set }
16 | }
17 |
18 | public struct CalendarDay {
19 | public var id = UUID()
20 | public var date: Date
21 | public var data: [CalendarData]
22 |
23 | public init(id: UUID = UUID(), date: Date, data: [CalendarData]) {
24 | self.id = id
25 | self.date = date
26 | self.data = data
27 | }
28 |
29 | public var descriptor: String {
30 | return "\(date.descriptor(.dayNumbered))"
31 | }
32 | }
33 |
34 | extension CalendarDay: Identifiable {
35 | public static func == (lhs: CalendarDay, rhs: CalendarDay) -> Bool {
36 | return lhs.id == rhs.id
37 | }
38 | }
39 |
40 | extension CalendarDay: Hashable {
41 | public func hash(into hasher: inout Hasher) {
42 | hasher.combine(id)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/MooCal/DataModels/CalendarMonth.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CalendarMonth.swift
3 | // CalyKitTestApp
4 | //
5 | // Created by Colby Mehmen on 10/19/23.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct CalendarMonth {
11 | public var id = UUID()
12 | public var startDate: Date
13 | public var days: [CalendarDay]
14 |
15 | public var title: String {
16 | startDate.descriptor(.month)
17 | }
18 |
19 | public var year: String {
20 | startDate.descriptor(.year)
21 | }
22 |
23 | public var startOfMonthOffset: Int {
24 | return startDate.monthStartOffset()
25 | }
26 |
27 | public var endOfMonthOffset: Int {
28 | return startDate.monthEndOffset()
29 | }
30 |
31 | static public func createCalendarMonth(fromMonthStartDate date: Date, data: [CalendarData]) -> CalendarMonth {
32 | let allDatesInCurrentMonth = date.getAllDatesInMonth()
33 | var days: [CalendarDay] = []
34 | for date in allDatesInCurrentMonth {
35 | days.append(.init(date: date, data: data.filter({$0.date.isInDay(date)})))
36 | }
37 | return .init(startDate: date, days: days)
38 | }
39 | }
40 |
41 | extension CalendarMonth: Hashable {
42 | public func hash(into hasher: inout Hasher) {
43 | hasher.combine(id)
44 | }
45 | }
46 |
47 | extension CalendarMonth: Identifiable {
48 | static public func == (lhs: CalendarMonth, rhs: CalendarMonth) -> Bool {
49 | return lhs.id == rhs.id
50 | }
51 | }
52 |
53 | extension CalendarMonth {
54 | static public func createCalendarMonths(fromMonthStartDates dates: [Date], data: [CalendarData]) -> [CalendarMonth] {
55 | return dates.compactMap({createCalendarMonth(fromMonthStartDate: $0, data: data)})
56 | }
57 |
58 | static public func createCalendarMonths(fromMonthStartDates dates: [Date], data: [CalendarData], completion: @escaping ( [CalendarMonth] ) -> ()) {
59 | let months = dates.compactMap({createCalendarMonth(fromMonthStartDate: $0, data: data)})
60 | completion(months)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/MooCal/Enums/CalendarDayOfWeekIndicatorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIView.swift
3 | //
4 | //
5 | // Created by Colby Mehmen on 5/18/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public enum CalendarDayOfWeekIndicatorView {
11 | case custom((DayOfWeek) -> (DayOfWeekIndicatorView))
12 | case singleLetter(DayOfWeekIndicatorView = EmptyView())
13 | case abreviated(DayOfWeekIndicatorView = EmptyView())
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/MooCal/Enums/CalendarDayStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Colby Mehmen on 1/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | //@available(iOS 16.0, *)
11 | //public struct ContributionPercentConfig {
12 | // public var value: Double // 100% value
13 | // public var color: Color
14 | //
15 | // public init(value: Double, color: Color) {
16 | // self.value = value
17 | // self.color = color
18 | // }
19 | //}
20 |
21 | //@available(iOS 16.0, *)
22 | //public enum CalendarDayStyle {
23 | //// case contribution(ContributionType)
24 | //// case circleGauge
25 | //// case custom
26 | // case classic
27 | //}
28 |
29 | //@available(iOS 16.0, *)
30 | //public enum ContributionType {
31 | // case count(ContributionConifg) // rename to count fon
32 | // case percent(ContributionPercentConfig)
33 | //}
34 |
--------------------------------------------------------------------------------
/Sources/MooCal/Enums/CalendarDayView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Colby Mehmen on 1/13/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public enum CalendarDayView {
11 | case custom((CalendarDay) -> (CustomView))
12 | case classic([any ClassicCalendarData])
13 | case numbered(ClassicNumberedDayViewConfig)
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/MooCal/Enums/CalendarMonthHeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIView.swift
3 | //
4 | //
5 | // Created by Colby Mehmen on 5/18/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // when using an enum without a `HeaderView` type (such as cusom), the compiler still needs to infer the `HeaderView` Type so that is why it is defaulted to `EmptyView` here, i don't necessarily like this, but it is required and cleaner on the implementation side. This is true for any added enum that does not provide a `HeaderView` type.
11 | public enum CalendarMonthHeaderView {
12 | case custom((CalendarMonth) -> (HeaderView))
13 | case monthLabel(HeaderView = EmptyView())
14 | case monthYearLabel(HeaderView = EmptyView())
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/MooCal/Enums/DayOfWeek.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DayOfWeek.swift
3 | // CalyKitTestApp
4 | //
5 | // Created by Colby Mehmen on 10/19/23.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum DayOfWeek: String, Identifiable {
11 | public var id: Self { return self }
12 |
13 | case monday
14 | case tuesday
15 | case wednesday
16 | case thursday
17 | case friday
18 | case saturday
19 | case sunday
20 |
21 | public var title: String {
22 | return self.rawValue.capitalized
23 | }
24 |
25 | public var letter: String {
26 | return self.rawValue.first?.uppercased() ?? ""
27 | }
28 |
29 | /**
30 | - Returns: returns abbreviated forms of the days of week. ex: 3 characters for all days of the week (mon, tue).
31 | */
32 | public var abbreviated: String {
33 | switch self {
34 | default:
35 | return String(self.rawValue.capitalized.prefix(3))
36 | }
37 | }
38 |
39 | public var offSet: Int {
40 | // if start of week is monday, else it would be same sequence but start from sunday
41 | switch self {
42 | case .monday:
43 | return 0
44 | case .tuesday:
45 | return 1
46 | case .wednesday:
47 | return 2
48 | case .thursday:
49 | return 3
50 | case .friday:
51 | return 4
52 | case .saturday:
53 | return 5
54 | case .sunday:
55 | return 6
56 | }
57 | }
58 |
59 | public static var allCases: [Self] {
60 | return [.monday, .tuesday, .wednesday, .thursday, .friday, .saturday, .sunday]
61 | }
62 |
63 | public static func allCasesStartingToday() -> [Self] {
64 | let today = DayOfWeek.create(from: Date())
65 | let startingIndex = Self.allCases.firstIndex(of: today)!
66 | let front: [DayOfWeek] = Array(allCases[0...startingIndex]).reversed() //m, t, w
67 | let back: [DayOfWeek] = Array(allCases[(startingIndex + 1).. Self {
91 | let dayOfWeek = date.descriptor(.dayOfWeek)
92 | for day in DayOfWeek.allCases {
93 | if day.title == dayOfWeek {
94 | return day
95 | }
96 | }
97 | fatalError()
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/MooCal/Extensions/Date+Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date+Ext.swift
3 | // Brush
4 | //
5 | // Created by Colby Mehmen on 2/8/23.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 |
12 | // Date extensions, static functions
13 | extension Date {
14 | /*
15 | @lhs -> the minuend
16 | @rhs -> the subtrahend
17 | @returns -> TimeInterval of the difference
18 | ex: if `lhs` is `Mar 3, 2023 at 11:45 PM` and `rhs` is `Mar 4, 2023 at 12:45 AM`
19 | a `TimeInterval` of `3600` will be returned
20 | **/
21 | static public func - (lhs: Date, rhs: Date) -> TimeInterval {
22 | return lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate
23 | }
24 |
25 | /*
26 | @date: -> date of the month where we will find the first date in.
27 | Static function that returns the first date of the
28 | month of the parameter date.
29 | ex: @Param:date = `Mar 3, 2023 at 10:22 PM`, the function will return `Mar 1, 2023 at 12:00 AM`
30 | **/
31 | static public func firstDateOfMonth(of date: Date) -> Date {
32 | let calendar = Calendar.current
33 | let date = calendar.date(from: Calendar.current.dateComponents([.year,.month], from: date))!
34 | return date
35 | }
36 |
37 | /*
38 | @date: -> date of the month where we will find the first date in.
39 | Static function that returns the last date of the
40 | month of the parameter date.
41 | ex: @Param:date = `Mar 3, 2023 at 10:22 PM`, the function will return `Mar 31, 2023 at 11:59 PM`
42 | **/
43 | static public func lastDateOfMonth(of date: Date) -> Date {
44 | let start = Calendar.current.date(from: Calendar.current.dateComponents([.year,.month], from: date))!
45 | var components = DateComponents()
46 | components.month = 1
47 | components.second = -1
48 | return Calendar.current.date(byAdding: components, to: start)!
49 | }
50 | }
51 |
52 | // Date extensions that return a string
53 | extension Date {
54 | /*
55 | A simple date formatter convenience enum
56 | `dayNumbered` -> `1`
57 | `dayOfWeek` -> `Monday`
58 | `month` -> `March`
59 | `year` => 2022
60 | **/
61 | public enum Descriptor {
62 | case dayNumbered
63 | case dayOfWeek
64 | case month
65 | case year
66 |
67 | public var dateFormatString: String {
68 | switch self {
69 | case .dayNumbered:
70 | return "dd"
71 | case .dayOfWeek:
72 | return "EEEE"
73 | case .month:
74 | return "MMMM"
75 | case .year:
76 | return "YYYY"
77 | }
78 | }
79 | }
80 |
81 | /*
82 | @descriptor -> the descriptor formate string to apply to the instance date
83 | ex: if date instance is `Mar 3, 2023 at 10:22 PM`
84 | `.dayNumbered` -> `3`
85 | `.dayOfWeek` -> `Friday`
86 | `.month` -> `March`
87 | `.year` -> `2023`
88 | **/
89 | public func descriptor(_ descriptor: Descriptor) -> String {
90 | let dateFormatter = DateFormatter()
91 | dateFormatter.dateFormat = descriptor.dateFormatString
92 | return dateFormatter.string(from: self)
93 | }
94 |
95 | public var timeFromNowFormatted: String {
96 | let minutesAgo = Int((Date() - self)/60)
97 | if minutesAgo < 60 {
98 | return "\(minutesAgo) m"
99 | } else if minutesAgo < (60 * 24) {
100 | return "\(Int(minutesAgo / 60)) h"
101 | } else {
102 | let days = Int(minutesAgo / (60 * 24))
103 | return "\(days) d"
104 | }
105 | }
106 | }
107 |
108 | // @returns -> Bool
109 | extension Date {
110 | /*
111 | Returns a boolean comparing if the date the instance method is applied
112 | to is in the same month as the parameter date.
113 | **/
114 | public func inSameMonth(as date: Date) -> Bool {
115 | return self.isInRange(
116 | start: Self.firstDateOfMonth(of: date),
117 | end: Self.lastDateOfMonth(of: date)
118 | )
119 | }
120 |
121 | /*
122 | @returns -> boolean if instance is greater/equal than `beginningDate` and less/equal to `endDate`.
123 | @beginnigDate: Date -> will check if instance date is greater than or equal to
124 | @endDate: Date -> will check if instance date is greater than or equal to
125 | **/
126 | public func isInRange(start beginningDate: Date, end endDate: Date) -> Bool {
127 | return self >= beginningDate && self <= endDate
128 | }
129 |
130 | /*
131 | @returns -> boolean value if instance date is in the same day as paramater date
132 | @date -> the date to compare the instance date to
133 | **/
134 | public func isInDay(_ date: Date) -> Bool {
135 | return self.isInRange(start: date.startOfDay, end: date.endOfDay)
136 | }
137 |
138 | /*
139 | @hourOfDay -> the hour of day to comapare to instance date **military hour (1-24)
140 | @returns -> boolean value of if instance date hour is before `hourOfDay`
141 | NOTE: hour of Day works in milirart time, it adds `hourOfDay` to the start of the instance date.
142 | ex: will return true if instance is `Mar 3, 2023 at 3:45 PM` and `hourOfDay` is `18` where
143 | `18` represents `Mar 3, 2023 at 6:00 PM` of the current calendar.
144 | **/
145 | public func isBefore(hourOfDay: Int) -> Bool {
146 | let beforeDate = Calendar.current.date(byAdding: .hour, value: hourOfDay, to: self.startOfDay)
147 | return self < beforeDate!
148 | }
149 |
150 | /*
151 | @hourOfDay -> the hour of day to comapare to instance date **military hour (1-24)
152 | @returns -> boolean value of if instance date hour is after `hourOfDay`
153 | NOTE: hour of Day works in milirary time, it adds `hourOfDay` to the start of the instance date.
154 | ex: will return false if instance is `Mar 3, 2023 at 3:45 PM` and `hourOfDay` is `18` where
155 | `18` represents `Mar 3, 2023 at 6:00 PM` of the current calendar.
156 | **/
157 | public func isAfter(hourOfDay: Int) -> Bool {
158 | let afterDate = Calendar.current.date(byAdding: .hour, value: hourOfDay, to: self.startOfDay)
159 | return self > afterDate!
160 | }
161 |
162 |
163 | /*
164 | @beginningHour -> the hour of day to comapare if instance hour is greater than to instance date **military hour (1-24)
165 | @endHour -> the hour of day to comapare if instance hour is less than to instance date **military hour (1-24)
166 | Note: if date is `Mar 3, 2023 at 3:45 PM` and `beginningHour` is 3 and `endHour` is 4 it will return true
167 | **/
168 | public func isInBetween(beginningHour: Int, endHour: Int) -> Bool {
169 | return self.isAfter(hourOfDay: beginningHour) && self.isBefore(hourOfDay: endHour)
170 | }
171 |
172 | /*
173 | @returns -> boolean value if instance date is in the same day the day
174 | as the time the code is executed.
175 | @date -> the date to compare the instance date to
176 | **/
177 | public var isToday: Bool {
178 | let now = Date()
179 | return self.isInDay(now)
180 | }
181 | }
182 |
183 | // @returns -> Date
184 | extension Date {
185 | /*
186 | Returns an array of Date Objects (starting at the beginning of the day) of every day
187 | in the month of the instance Date.
188 | ex: a date in february, returns a date for each day (28 Date objects starting at 12:00am).
189 | **/
190 | public func getAllDatesInMonth() -> [Date] {
191 | let calendar = Calendar.current
192 | let startOfMonth = Date.firstDateOfMonth(of: self)
193 | let range = calendar.range(of: .day, in: .month, for: startOfMonth)! // This returns a Range for the month based off the start date.
194 | return range.compactMap { dayCount -> Date in
195 | return calendar.date(byAdding: .day, value: (dayCount - 1), to: startOfMonth)!
196 | } // returns an array of dates for given month, starting at midnight
197 | }
198 |
199 | /*
200 | @value -> Amount of calendar component to change by
201 | @component -> Calendary.Component [.day, month, year], to change by.
202 | @ returns a date object that is a result of adding the `value`
203 | of `components` to the instance date.
204 | **/
205 | public func changed(by value: Int, component: Calendar.Component) -> Date {
206 | return Calendar.current.date(byAdding: component, value: value, to: self)!
207 | }
208 |
209 | /*
210 | Returns a date object starting at 12:00am
211 | **/
212 | public var startOfDay: Date {
213 | return Calendar.current.startOfDay(for: self)
214 | }
215 |
216 | /*
217 | Returns a date object starting at 11:59pm
218 | **/
219 | public var endOfDay: Date {
220 | var components = DateComponents()
221 | components.day = 1
222 | components.second = -1
223 | return Calendar.current.date(
224 | byAdding: components,
225 | to: Calendar.current.startOfDay(for: self)
226 | )!
227 | }
228 |
229 | /*
230 | This returns the calendar month `day offset`, this is the count of days befor the first
231 | day of the month in a calendar view.
232 | ex: if the first date of the month is on a a `Wednesday` -> `Mar 1, 2023 at 12:00 AM` then
233 | the off set will be `2` if the week starts on monday, and `3` if the week starts on
234 | sunday. 2 -> **WRFSS (**1,2,3,4,5) and 2 -> ***WRFS (***1,2,3,4). This is mainly used for
235 | drawing calendars
236 | **/
237 | public func monthStartOffset() -> Int {
238 | let date = Date.firstDateOfMonth(of: self)
239 | return DayOfWeek.create(from: date).offSet
240 | }
241 |
242 | /*
243 | this is the inverse of `monthStartOffset` find the last date of the month,
244 | and then returns the `inverse` of how many days are in the last week.
245 | ex: if the last date of the month is -> `Mar 31, 2023 at 11:59 PM` a `Friday`
246 | if the week begins on `monday` it will return `2` and if the week starts on
247 | sunday it will return a `1`. 2 -> MTWRF** (28,29,30,31,*, *) and 2 -> SMTWRFS* (27,28,29,30,31,*).
248 | This is mainly used for drawing calendars
249 | **/
250 | public func monthEndOffset() -> Int {
251 | let date = Date.lastDateOfMonth(of: self)
252 | let index = DayOfWeek.create(from: date).offSet
253 | return 6 - index
254 | }
255 |
256 | public func startOfMonth() -> Date {
257 | let calendar = Calendar.current
258 | let components = calendar.dateComponents([.year, .month], from: self)
259 | return calendar.date(from: components)!.startOfDay
260 | }
261 | }
262 |
263 |
264 | extension Date {
265 | public static func firstDateOfAllMonthsIn(start: Date, nextMonths: Int) -> [Date] {
266 | var monthDates: [Date] = [] // this is an array of dates starting at the first day of each month
267 | let todaysDateOfMonth = Date.firstDateOfMonth(of: start)
268 |
269 | for i in 0.. [Date] {
278 | var monthDates: [Date] = [] // this is an array of dates starting at the first day of each month
279 | let todaysDateOfMonth = Date.firstDateOfMonth(of: start)
280 |
281 | for i in 1.. Date {
295 | var dateComponent = DateComponents()
296 | switch component {
297 | case .day:
298 | dateComponent.day = value
299 | case .month:
300 | dateComponent.month = value
301 | case .weekOfYear, .weekOfMonth:
302 | dateComponent.weekOfYear = value
303 | case .year:
304 | dateComponent.year = value
305 | default:
306 | break
307 | }
308 | return Calendar.current.date(byAdding: dateComponent, to: self) ?? self
309 | }
310 | }
311 |
312 |
--------------------------------------------------------------------------------
/Sources/MooCal/MooCal.swift:
--------------------------------------------------------------------------------
1 | // The Swift Programming Language
2 | // https://docs.swift.org/swift-book
3 |
--------------------------------------------------------------------------------
/Sources/MooCal/Protocols/CalendarData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CalendarData.swift
3 | // CalyKitTestApp
4 | //
5 | // Created by Colby Mehmen on 10/19/23.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol CalendarData {
11 | var date: Date { get set }
12 | //var value: Double { get set }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/MooCal/Views/DayViews/ClassicCalendarDayView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClassicCalendarDayView.swift
3 | // CalyKitTestApp
4 | //
5 | // Created by Colby Mehmen on 10/22/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct ClassicNumberedDayViewConfig {
11 | public var textColor: Color
12 | public var backgroundColor: Color
13 | public var highlightToday: Bool
14 | public var highlightColor: Color
15 | public var cornerRadius: Double
16 |
17 | public init(textColor: Color = .white, backgroundColor: Color = .gray.opacity(0.33), highlightToday: Bool = true, highlightColor: Color = .blue, cornerRadius: Double = 10.0) {
18 | self.textColor = textColor
19 | self.backgroundColor = backgroundColor
20 | self.highlightToday = highlightToday
21 | self.highlightColor = highlightColor
22 | self.cornerRadius = cornerRadius
23 | }
24 | }
25 |
26 | public struct ClassicNumberedDayView: View {
27 | public var day: CalendarDay
28 | public var config: ClassicNumberedDayViewConfig
29 |
30 | public var body: some View {
31 | ZStack {
32 | RoundedRectangle(cornerRadius: config.cornerRadius)
33 | .aspectRatio(contentMode: .fit)
34 | .foregroundStyle(config.backgroundColor)
35 |
36 | Text(day.descriptor)
37 | .font(.caption)
38 | .bold()
39 | .foregroundStyle(config.textColor)
40 | }
41 | .overlay {
42 | if config.highlightToday && day.date.isToday {
43 | RoundedRectangle(cornerRadius: config.cornerRadius)
44 | .stroke(config.highlightColor, lineWidth: 3)
45 | }
46 | }
47 | }
48 | }
49 |
50 | @available(iOS 16.0, *)
51 | public struct ClassicCalendarDayView/**/: View {
52 | public var day: CalendarDay
53 | public var items: [any ClassicCalendarData]
54 |
55 | public var body: some View {
56 | HStack(spacing: 0.0) {
57 | ZStack {
58 | RoundedRectangle(cornerRadius: 5.0)
59 | .foregroundStyle(.clear)
60 | .opacity(0.5)
61 | .aspectRatio(contentMode: .fit)
62 | VStack(spacing: 0.0) {
63 | HStack(spacing: 0.0) {
64 | Spacer()
65 | Text("\(day.date.descriptor(.dayNumbered))")
66 | .font(.caption)
67 | .bold()
68 | .foregroundStyle(.white)
69 | }
70 | .padding(.trailing, 4)
71 | //
72 | // ForEach(0.. some View {
89 | HStack(spacing: 2.0) {
90 | Circle()
91 | .frame(width: 5.0, height: 5.0)
92 | .foregroundStyle(item.color)
93 | Text(item.title)
94 | .lineLimit(1)
95 | .foregroundStyle(.primary)
96 | .font(.custom("", size: 5))
97 | Spacer()
98 | }
99 | //.padding(.horizontal, 2)
100 | }
101 | }
102 |
103 | //#Preview {
104 | // let startOfMonth = Date().startOfMonth()
105 | // let dates = startOfMonth.getAllDatesInMonth()
106 | // let data = dates.compactMap({$0.getRandomEvent()})
107 | // let calendarDays = startOfMonth.getAllDatesInMonth().compactMap({
108 | // CalendarDay(date: $0, data: [])
109 | // })
110 | //
111 | // return CalendarMonthView(
112 | // calendarMonth: .init(startDate: startOfMonth, days: calendarDays)) { day in
113 | // let items = data.filter({$0.date.isInDay(day.date)})
114 | //
115 | // return ZStack {
116 | // Color.red
117 | // .clipShape(Circle())
118 | // .cornerRadius(10.0)
119 | // .frame(width: 50.0, height: 50.0)
120 | // Text(day.descriptor)
121 | // .foregroundStyle(.white)
122 | // .bold()
123 | // }
124 | // } onSelection: { selectedDay in
125 | // //
126 | // }
127 | //}
128 |
129 |
--------------------------------------------------------------------------------
/Sources/MooCal/Views/TimeViews/CalendarMonthView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CalendarMonthView.swift
3 | // CalyKitTestApp
4 | //
5 | // Created by Colby Mehmen on 10/19/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct CalendarMonthView: View {
11 | public var calendarMonth: CalendarMonth
12 | public var calendarHeaderView: CalendarMonthHeaderView
13 | public var calendarDayView: CalendarDayView
14 | public var calendarDayOfWeekInidcatorView: CalendarDayOfWeekIndicatorView
15 | public var onSelection: ((CalendarDay) -> ())?
16 |
17 | public init(calendarMonth: CalendarMonth, calendarHeaderView: CalendarMonthHeaderView, calendarDayView: CalendarDayView, calendarDayOfWeekInidcatorView: CalendarDayOfWeekIndicatorView, onSelection: ( (CalendarDay) -> Void)? = nil) {
18 | self.calendarMonth = calendarMonth
19 | self.calendarHeaderView = calendarHeaderView
20 | self.calendarDayView = calendarDayView
21 | self.calendarDayOfWeekInidcatorView = calendarDayOfWeekInidcatorView
22 | self.onSelection = onSelection
23 | }
24 |
25 | public var body: some View {
26 | VStack(spacing: 0.0) {
27 | headerView
28 | daysView
29 | .padding()
30 | }
31 | }
32 |
33 | @ViewBuilder
34 | private var headerView: some View {
35 | switch calendarHeaderView {
36 | case .custom(let customHeaderView):
37 | customHeaderView(calendarMonth)
38 | case .monthLabel:
39 | Text(calendarMonth.title)
40 | .bold()
41 | case .monthYearLabel:
42 | Text("\(calendarMonth.title) \(calendarMonth.year)")
43 | .bold()
44 | }
45 | }
46 |
47 | @ViewBuilder
48 | private var daysOfWeek: some View {
49 | ForEach(DayOfWeek.allCases) { day in
50 | switch self.calendarDayOfWeekInidcatorView {
51 | case .custom(let customCalendarDayOFWeekIndicatorView):
52 | customCalendarDayOFWeekIndicatorView(day)
53 | case .singleLetter:
54 | Text("\(day.letter)")
55 | case .abreviated:
56 | Text("\(day.abbreviated)")
57 | }
58 | }
59 | }
60 |
61 | var trailingEmptyDayCount: Int {
62 | let d = (offSet + calendarMonth.days.count) % 7
63 | return 7 - d
64 | }
65 |
66 | public var offSet: Int {
67 | calendarMonth.startDate.monthStartOffset()
68 | }
69 |
70 | @ViewBuilder
71 | public var daysView: some View {
72 | LazyVGrid(columns: Array(repeating: GridItem(), count: 7)) {
73 | daysOfWeek
74 | ForEach(0.. some View {
105 | switch calendarDayView {
106 | case .custom(let customView):
107 | customView(day)
108 |
109 | case .classic(let data):
110 | let filteredItems = data.filter({$0.date.isInDay(day.date)})
111 | ClassicCalendarDayView(day: day, items: filteredItems)
112 |
113 | case .numbered(let config):
114 | ClassicNumberedDayView(day: day, config: config)
115 | }
116 | }
117 |
118 | // @ViewBuilder
119 | // public func contributionDayView(calendarDay: CalendarDay, contributionType: ContributionType) -> some View {
120 | // switch contributionType {
121 | // case .count(let contributionConfig):
122 | // ContributionDayView(calendarDay: calendarDay, contributionConfig: contributionConfig)
123 | //
124 | // case .percent(let contributionPercentConfig):
125 | // completionGaugeView(calendarDay: calendarDay, contributionPercentConfig: contributionPercentConfig)
126 | // }
127 | // }
128 |
129 | // private func completionGaugeView(calendarDay: CalendarDay, contributionPercentConfig: ContributionPercentConfig) -> some View {
130 | //
131 | // print("max: \(contributionPercentConfig.value)")
132 | // print("\(calendarDay.date.formatted()) - \(calendarDay.sum)")
133 | // return CompletionGaugeView(color: contributionPercentConfig.color, min: 0, max: contributionPercentConfig.value, value: calendarDay.sum)
134 | // .background {
135 | // if calendarDay.date.isToday {
136 | // Color.gray.opacity(0.35)
137 | // }
138 | // }
139 | // }
140 | }
141 |
--------------------------------------------------------------------------------
/Sources/MooCal/Views/TimeViews/CalendarYearView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIView.swift
3 | //
4 | //
5 | // Created by Colby Mehmen on 1/13/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct CalendarYearViewMoo: View {
11 | public var calendarMonths: [CalendarMonth]
12 | public var calendarHeaderView: CalendarMonthHeaderView
13 | public var calendarDayView: CalendarDayView
14 | public var calendarDayOfWeekInidcatorView: CalendarDayOfWeekIndicatorView
15 | public var onSelection: ((CalendarDay) -> ())?
16 |
17 | public init(calendarMonths: [CalendarMonth], calendarHeaderView: CalendarMonthHeaderView, calendarDayView: CalendarDayView, calendarDayOfWeekInidcatorView: CalendarDayOfWeekIndicatorView, onSelection: ( (CalendarDay) -> Void)? = nil) {
18 | self.calendarMonths = calendarMonths
19 | self.calendarDayView = calendarDayView
20 | self.calendarHeaderView = calendarHeaderView
21 | self.calendarDayOfWeekInidcatorView = calendarDayOfWeekInidcatorView
22 | self.onSelection = onSelection
23 | }
24 |
25 | public var body: some View {
26 | ForEach(calendarMonths) { calendarMonth in
27 | CalendarMonthView(
28 | calendarMonth: calendarMonth,
29 | calendarHeaderView: calendarHeaderView,
30 | calendarDayView: calendarDayView,
31 | calendarDayOfWeekInidcatorView: calendarDayOfWeekInidcatorView,
32 | onSelection: onSelection
33 | )
34 | .id(calendarMonth.id)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/MooCal/Views/TimeViews/ScrollableCalendarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIView.swift
3 | //
4 | //
5 | // Created by Colby Mehmen on 1/13/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct ScrollableCalendarView: View {
11 | @ObservedObject var viewModel: ScrollableCalendarViewViewModel
12 |
13 | public var calendarHeaderView: CalendarMonthHeaderView
14 | public var calendarDayView: CalendarDayView
15 | public var calendarDayOfWeekInidcatorView: CalendarDayOfWeekIndicatorView
16 | public var onSelection: ((CalendarDay) -> ())?
17 |
18 | public init(viewModel: ScrollableCalendarViewViewModel = ScrollableCalendarViewViewModel(), calendarHeaderView: CalendarMonthHeaderView = .monthLabel(), calendarDayView: CalendarDayView, calendarDayOfWeekInidcatorView: CalendarDayOfWeekIndicatorView = .singleLetter(), onSelection: ( (CalendarDay) -> Void)? = nil) {
19 | self.viewModel = viewModel
20 | self.calendarHeaderView = calendarHeaderView
21 | self.calendarDayView = calendarDayView
22 | self.calendarDayOfWeekInidcatorView = calendarDayOfWeekInidcatorView
23 | self.onSelection = onSelection
24 | }
25 |
26 | public var body: some View {
27 | ScrollViewReader { proxy in
28 | ScrollView {
29 | HStack {
30 | Spacer()
31 | Button(action: {
32 | viewModel.addMonth(.toBeginning)
33 | }, label: {
34 | VStack {
35 | Image(systemName: "arrow.up.circle.fill")
36 | .foregroundStyle(.secondary)
37 | .bold()
38 | Text("See More")
39 | .foregroundStyle(.secondary)
40 | .bold()
41 | }
42 | })
43 | .buttonStyle(.plain)
44 | .listRowBackground(Color.clear)
45 | Spacer()
46 | }
47 | .listRowBackground(Color.clear)
48 | .padding(.vertical, 20)
49 |
50 | CalendarYearViewMoo(
51 | calendarMonths: viewModel.calendarMonths,
52 | calendarHeaderView: calendarHeaderView,
53 | calendarDayView: calendarDayView,
54 | calendarDayOfWeekInidcatorView: calendarDayOfWeekInidcatorView,
55 | onSelection: onSelection
56 | )
57 | HStack {
58 | Spacer()
59 | Button(action: {
60 | viewModel.addMonth(.toBeginning)
61 | }, label: {
62 | VStack {
63 | Text("See More")
64 | .foregroundStyle(.secondary)
65 | .bold()
66 |
67 | Image(systemName: "arrow.down.circle.fill")
68 | .foregroundStyle(.secondary)
69 | .bold()
70 | //.font(.caption)
71 | }
72 | })
73 | .listRowBackground(Color.clear)
74 | .buttonStyle(.plain)
75 | Spacer()
76 | }
77 | .listRowBackground(Color.clear)
78 | .padding(.vertical, 20)
79 | }
80 | .scrollIndicators(.hidden)
81 | .onAppear {
82 | guard let currentMonthId = viewModel.currentMonthId else { return }
83 | proxy.scrollTo(currentMonthId, anchor: .top)
84 | }
85 | }
86 | }
87 | }
88 |
89 | public class ScrollableCalendarViewViewModel: ObservableObject {
90 | @Published public var calendarMonths: [CalendarMonth]
91 |
92 | public var currentMonthId: UUID? {
93 | guard let currentCalendarMonth = calendarMonths.first(where: {$0.startDate.inSameMonth(as: Date())}) else { print("returning nil"); return nil }
94 | return currentCalendarMonth.id
95 | }
96 |
97 | public init(calendarMonths: [CalendarMonth]) {
98 | self.calendarMonths = calendarMonths
99 | }
100 |
101 | public init(currentDate: Date = Date(), preMonths: Int = 6, postMonths: Int = 6) {
102 | let dates = Date.firstDateOfAllMonthsIn(start: currentDate, preMonths: preMonths, postMonths: postMonths)
103 | self.calendarMonths = CalendarMonth.createCalendarMonths(fromMonthStartDates: dates, data: [])
104 | }
105 |
106 | public func addMonth(_ action: MonthAction) {
107 | guard let month = action.calendarMonthForIncrementing(calendarMonths: calendarMonths) else {
108 | return
109 | }
110 | let date = month.startDate
111 | let newDate = date.modified(by: action.incrementor, component: .month)
112 | let newMonth = CalendarMonth.createCalendarMonth(fromMonthStartDate: newDate, data: [])
113 |
114 | switch action {
115 | case .toBeginning:
116 | self.calendarMonths.insert(newMonth, at: 0)
117 | case .toEnd:
118 | self.calendarMonths.append(newMonth)
119 | }
120 | }
121 |
122 | public func addMonth(_ action: MonthAction, data: [CalendarData]) {
123 | guard let month = action.calendarMonthForIncrementing(calendarMonths: calendarMonths) else {
124 | return
125 | }
126 | let date = month.startDate
127 | let newDate = date.modified(by: action.incrementor, component: .month)
128 | let newMonth = CalendarMonth.createCalendarMonth(fromMonthStartDate: newDate, data: data)
129 |
130 | switch action {
131 | case .toBeginning:
132 | self.calendarMonths.insert(newMonth, at: 0)
133 | case .toEnd:
134 | self.calendarMonths.append(newMonth)
135 | }
136 | }
137 |
138 | public enum MonthAction {
139 | case toBeginning
140 | case toEnd
141 |
142 | var incrementor: Int {
143 | switch self {
144 | case .toBeginning:
145 | return -1
146 | case .toEnd:
147 | return 1
148 | }
149 | }
150 |
151 | public func calendarMonthForIncrementing(calendarMonths: [CalendarMonth]) -> CalendarMonth? {
152 | switch self {
153 | case .toBeginning:
154 | return calendarMonths.first
155 | case .toEnd:
156 | return calendarMonths.last
157 | }
158 | }
159 | }
160 | }
161 |
162 | //#Preview {
163 | // SwiftUIView()
164 | //}
165 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 778C95BA2B562BA2008A151C /* ContentViewTwo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778C95B92B562BA2008A151C /* ContentViewTwo.swift */; };
11 | 778C95BC2B562D8D008A151C /* ContentViewThree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778C95BB2B562D8D008A151C /* ContentViewThree.swift */; };
12 | 779CE13E2C05234400E73E73 /* ContentViewFour.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779CE13D2C05234400E73E73 /* ContentViewFour.swift */; };
13 | 77D3E85F2B54DE5F00DEAC1E /* MooCalTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D3E85E2B54DE5F00DEAC1E /* MooCalTestAppApp.swift */; };
14 | 77D3E8612B54DE5F00DEAC1E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D3E8602B54DE5F00DEAC1E /* ContentView.swift */; };
15 | 77D3E8632B54DE6200DEAC1E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 77D3E8622B54DE6200DEAC1E /* Assets.xcassets */; };
16 | 77D3E8662B54DE6200DEAC1E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 77D3E8652B54DE6200DEAC1E /* Preview Assets.xcassets */; };
17 | 77D3E8702B54E06700DEAC1E /* MooCal in Frameworks */ = {isa = PBXBuildFile; productRef = 77D3E86F2B54E06700DEAC1E /* MooCal */; };
18 | 77D3E8732B54E11600DEAC1E /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D3E8722B54E11600DEAC1E /* Event.swift */; };
19 | 77D3E8762B54E13900DEAC1E /* EventInputSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D3E8752B54E13900DEAC1E /* EventInputSheet.swift */; };
20 | 77D3E8782B54E16B00DEAC1E /* DayEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D3E8772B54E16B00DEAC1E /* DayEventView.swift */; };
21 | /* End PBXBuildFile section */
22 |
23 | /* Begin PBXFileReference section */
24 | 778C95B92B562BA2008A151C /* ContentViewTwo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewTwo.swift; sourceTree = ""; };
25 | 778C95BB2B562D8D008A151C /* ContentViewThree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewThree.swift; sourceTree = ""; };
26 | 779CE13D2C05234400E73E73 /* ContentViewFour.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContentViewFour.swift; path = MooCalTestApp/ContentViewFour.swift; sourceTree = ""; };
27 | 77D3E85B2B54DE5F00DEAC1E /* MooCalTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MooCalTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
28 | 77D3E85E2B54DE5F00DEAC1E /* MooCalTestAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MooCalTestAppApp.swift; sourceTree = ""; };
29 | 77D3E8602B54DE5F00DEAC1E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
30 | 77D3E8622B54DE6200DEAC1E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
31 | 77D3E8652B54DE6200DEAC1E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
32 | 77D3E86C2B54DF2E00DEAC1E /* MooCal */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MooCal; path = ../..; sourceTree = ""; };
33 | 77D3E8722B54E11600DEAC1E /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; };
34 | 77D3E8752B54E13900DEAC1E /* EventInputSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventInputSheet.swift; sourceTree = ""; };
35 | 77D3E8772B54E16B00DEAC1E /* DayEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayEventView.swift; sourceTree = ""; };
36 | /* End PBXFileReference section */
37 |
38 | /* Begin PBXFrameworksBuildPhase section */
39 | 77D3E8582B54DE5F00DEAC1E /* Frameworks */ = {
40 | isa = PBXFrameworksBuildPhase;
41 | buildActionMask = 2147483647;
42 | files = (
43 | 77D3E8702B54E06700DEAC1E /* MooCal in Frameworks */,
44 | );
45 | runOnlyForDeploymentPostprocessing = 0;
46 | };
47 | /* End PBXFrameworksBuildPhase section */
48 |
49 | /* Begin PBXGroup section */
50 | 77D3E8522B54DE5F00DEAC1E = {
51 | isa = PBXGroup;
52 | children = (
53 | 779CE13D2C05234400E73E73 /* ContentViewFour.swift */,
54 | 77D3E86C2B54DF2E00DEAC1E /* MooCal */,
55 | 77D3E85D2B54DE5F00DEAC1E /* MooCalTestApp */,
56 | 77D3E85C2B54DE5F00DEAC1E /* Products */,
57 | 77D3E86E2B54E06700DEAC1E /* Frameworks */,
58 | );
59 | sourceTree = "";
60 | };
61 | 77D3E85C2B54DE5F00DEAC1E /* Products */ = {
62 | isa = PBXGroup;
63 | children = (
64 | 77D3E85B2B54DE5F00DEAC1E /* MooCalTestApp.app */,
65 | );
66 | name = Products;
67 | sourceTree = "";
68 | };
69 | 77D3E85D2B54DE5F00DEAC1E /* MooCalTestApp */ = {
70 | isa = PBXGroup;
71 | children = (
72 | 77D3E8742B54E12A00DEAC1E /* Views */,
73 | 77D3E8712B54E10B00DEAC1E /* DataModels */,
74 | 77D3E85E2B54DE5F00DEAC1E /* MooCalTestAppApp.swift */,
75 | 778C95BB2B562D8D008A151C /* ContentViewThree.swift */,
76 | 778C95B92B562BA2008A151C /* ContentViewTwo.swift */,
77 | 77D3E8602B54DE5F00DEAC1E /* ContentView.swift */,
78 | 77D3E8622B54DE6200DEAC1E /* Assets.xcassets */,
79 | 77D3E8642B54DE6200DEAC1E /* Preview Content */,
80 | );
81 | path = MooCalTestApp;
82 | sourceTree = "";
83 | };
84 | 77D3E8642B54DE6200DEAC1E /* Preview Content */ = {
85 | isa = PBXGroup;
86 | children = (
87 | 77D3E8652B54DE6200DEAC1E /* Preview Assets.xcassets */,
88 | );
89 | path = "Preview Content";
90 | sourceTree = "";
91 | };
92 | 77D3E86E2B54E06700DEAC1E /* Frameworks */ = {
93 | isa = PBXGroup;
94 | children = (
95 | );
96 | name = Frameworks;
97 | sourceTree = "";
98 | };
99 | 77D3E8712B54E10B00DEAC1E /* DataModels */ = {
100 | isa = PBXGroup;
101 | children = (
102 | 77D3E8722B54E11600DEAC1E /* Event.swift */,
103 | );
104 | path = DataModels;
105 | sourceTree = "";
106 | };
107 | 77D3E8742B54E12A00DEAC1E /* Views */ = {
108 | isa = PBXGroup;
109 | children = (
110 | 77D3E8752B54E13900DEAC1E /* EventInputSheet.swift */,
111 | 77D3E8772B54E16B00DEAC1E /* DayEventView.swift */,
112 | );
113 | path = Views;
114 | sourceTree = "";
115 | };
116 | /* End PBXGroup section */
117 |
118 | /* Begin PBXNativeTarget section */
119 | 77D3E85A2B54DE5F00DEAC1E /* MooCalTestApp */ = {
120 | isa = PBXNativeTarget;
121 | buildConfigurationList = 77D3E8692B54DE6200DEAC1E /* Build configuration list for PBXNativeTarget "MooCalTestApp" */;
122 | buildPhases = (
123 | 77D3E8572B54DE5F00DEAC1E /* Sources */,
124 | 77D3E8582B54DE5F00DEAC1E /* Frameworks */,
125 | 77D3E8592B54DE5F00DEAC1E /* Resources */,
126 | );
127 | buildRules = (
128 | );
129 | dependencies = (
130 | );
131 | name = MooCalTestApp;
132 | packageProductDependencies = (
133 | 77D3E86F2B54E06700DEAC1E /* MooCal */,
134 | );
135 | productName = MooCalTestApp;
136 | productReference = 77D3E85B2B54DE5F00DEAC1E /* MooCalTestApp.app */;
137 | productType = "com.apple.product-type.application";
138 | };
139 | /* End PBXNativeTarget section */
140 |
141 | /* Begin PBXProject section */
142 | 77D3E8532B54DE5F00DEAC1E /* Project object */ = {
143 | isa = PBXProject;
144 | attributes = {
145 | BuildIndependentTargetsInParallel = 1;
146 | LastSwiftUpdateCheck = 1500;
147 | LastUpgradeCheck = 1500;
148 | TargetAttributes = {
149 | 77D3E85A2B54DE5F00DEAC1E = {
150 | CreatedOnToolsVersion = 15.0;
151 | };
152 | };
153 | };
154 | buildConfigurationList = 77D3E8562B54DE5F00DEAC1E /* Build configuration list for PBXProject "MooCalTestApp" */;
155 | compatibilityVersion = "Xcode 14.0";
156 | developmentRegion = en;
157 | hasScannedForEncodings = 0;
158 | knownRegions = (
159 | en,
160 | Base,
161 | );
162 | mainGroup = 77D3E8522B54DE5F00DEAC1E;
163 | productRefGroup = 77D3E85C2B54DE5F00DEAC1E /* Products */;
164 | projectDirPath = "";
165 | projectRoot = "";
166 | targets = (
167 | 77D3E85A2B54DE5F00DEAC1E /* MooCalTestApp */,
168 | );
169 | };
170 | /* End PBXProject section */
171 |
172 | /* Begin PBXResourcesBuildPhase section */
173 | 77D3E8592B54DE5F00DEAC1E /* Resources */ = {
174 | isa = PBXResourcesBuildPhase;
175 | buildActionMask = 2147483647;
176 | files = (
177 | 77D3E8662B54DE6200DEAC1E /* Preview Assets.xcassets in Resources */,
178 | 77D3E8632B54DE6200DEAC1E /* Assets.xcassets in Resources */,
179 | );
180 | runOnlyForDeploymentPostprocessing = 0;
181 | };
182 | /* End PBXResourcesBuildPhase section */
183 |
184 | /* Begin PBXSourcesBuildPhase section */
185 | 77D3E8572B54DE5F00DEAC1E /* Sources */ = {
186 | isa = PBXSourcesBuildPhase;
187 | buildActionMask = 2147483647;
188 | files = (
189 | 779CE13E2C05234400E73E73 /* ContentViewFour.swift in Sources */,
190 | 77D3E8612B54DE5F00DEAC1E /* ContentView.swift in Sources */,
191 | 77D3E8732B54E11600DEAC1E /* Event.swift in Sources */,
192 | 77D3E85F2B54DE5F00DEAC1E /* MooCalTestAppApp.swift in Sources */,
193 | 778C95BA2B562BA2008A151C /* ContentViewTwo.swift in Sources */,
194 | 77D3E8762B54E13900DEAC1E /* EventInputSheet.swift in Sources */,
195 | 77D3E8782B54E16B00DEAC1E /* DayEventView.swift in Sources */,
196 | 778C95BC2B562D8D008A151C /* ContentViewThree.swift in Sources */,
197 | );
198 | runOnlyForDeploymentPostprocessing = 0;
199 | };
200 | /* End PBXSourcesBuildPhase section */
201 |
202 | /* Begin XCBuildConfiguration section */
203 | 77D3E8672B54DE6200DEAC1E /* Debug */ = {
204 | isa = XCBuildConfiguration;
205 | buildSettings = {
206 | ALWAYS_SEARCH_USER_PATHS = NO;
207 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
208 | CLANG_ANALYZER_NONNULL = YES;
209 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
210 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
211 | CLANG_ENABLE_MODULES = YES;
212 | CLANG_ENABLE_OBJC_ARC = YES;
213 | CLANG_ENABLE_OBJC_WEAK = YES;
214 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
215 | CLANG_WARN_BOOL_CONVERSION = YES;
216 | CLANG_WARN_COMMA = YES;
217 | CLANG_WARN_CONSTANT_CONVERSION = YES;
218 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
219 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
220 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
221 | CLANG_WARN_EMPTY_BODY = YES;
222 | CLANG_WARN_ENUM_CONVERSION = YES;
223 | CLANG_WARN_INFINITE_RECURSION = YES;
224 | CLANG_WARN_INT_CONVERSION = YES;
225 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
226 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
227 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
228 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
229 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
230 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
231 | CLANG_WARN_STRICT_PROTOTYPES = YES;
232 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
233 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
234 | CLANG_WARN_UNREACHABLE_CODE = YES;
235 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
236 | COPY_PHASE_STRIP = NO;
237 | DEBUG_INFORMATION_FORMAT = dwarf;
238 | ENABLE_STRICT_OBJC_MSGSEND = YES;
239 | ENABLE_TESTABILITY = YES;
240 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
241 | GCC_C_LANGUAGE_STANDARD = gnu17;
242 | GCC_DYNAMIC_NO_PIC = NO;
243 | GCC_NO_COMMON_BLOCKS = YES;
244 | GCC_OPTIMIZATION_LEVEL = 0;
245 | GCC_PREPROCESSOR_DEFINITIONS = (
246 | "DEBUG=1",
247 | "$(inherited)",
248 | );
249 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
250 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
251 | GCC_WARN_UNDECLARED_SELECTOR = YES;
252 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
253 | GCC_WARN_UNUSED_FUNCTION = YES;
254 | GCC_WARN_UNUSED_VARIABLE = YES;
255 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
256 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
257 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
258 | MTL_FAST_MATH = YES;
259 | ONLY_ACTIVE_ARCH = YES;
260 | SDKROOT = iphoneos;
261 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
262 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
263 | };
264 | name = Debug;
265 | };
266 | 77D3E8682B54DE6200DEAC1E /* Release */ = {
267 | isa = XCBuildConfiguration;
268 | buildSettings = {
269 | ALWAYS_SEARCH_USER_PATHS = NO;
270 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
271 | CLANG_ANALYZER_NONNULL = YES;
272 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
273 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
274 | CLANG_ENABLE_MODULES = YES;
275 | CLANG_ENABLE_OBJC_ARC = YES;
276 | CLANG_ENABLE_OBJC_WEAK = YES;
277 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
278 | CLANG_WARN_BOOL_CONVERSION = YES;
279 | CLANG_WARN_COMMA = YES;
280 | CLANG_WARN_CONSTANT_CONVERSION = YES;
281 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
282 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
283 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
284 | CLANG_WARN_EMPTY_BODY = YES;
285 | CLANG_WARN_ENUM_CONVERSION = YES;
286 | CLANG_WARN_INFINITE_RECURSION = YES;
287 | CLANG_WARN_INT_CONVERSION = YES;
288 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
289 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
290 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
291 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
292 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
293 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
294 | CLANG_WARN_STRICT_PROTOTYPES = YES;
295 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
296 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
297 | CLANG_WARN_UNREACHABLE_CODE = YES;
298 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
299 | COPY_PHASE_STRIP = NO;
300 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
301 | ENABLE_NS_ASSERTIONS = NO;
302 | ENABLE_STRICT_OBJC_MSGSEND = YES;
303 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
304 | GCC_C_LANGUAGE_STANDARD = gnu17;
305 | GCC_NO_COMMON_BLOCKS = YES;
306 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
307 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
308 | GCC_WARN_UNDECLARED_SELECTOR = YES;
309 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
310 | GCC_WARN_UNUSED_FUNCTION = YES;
311 | GCC_WARN_UNUSED_VARIABLE = YES;
312 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
313 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
314 | MTL_ENABLE_DEBUG_INFO = NO;
315 | MTL_FAST_MATH = YES;
316 | SDKROOT = iphoneos;
317 | SWIFT_COMPILATION_MODE = wholemodule;
318 | VALIDATE_PRODUCT = YES;
319 | };
320 | name = Release;
321 | };
322 | 77D3E86A2B54DE6200DEAC1E /* Debug */ = {
323 | isa = XCBuildConfiguration;
324 | buildSettings = {
325 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
326 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
327 | CODE_SIGN_STYLE = Automatic;
328 | CURRENT_PROJECT_VERSION = 1;
329 | DEVELOPMENT_ASSET_PATHS = "\"MooCalTestApp/Preview Content\"";
330 | DEVELOPMENT_TEAM = KPK2BX5PW8;
331 | ENABLE_PREVIEWS = YES;
332 | GENERATE_INFOPLIST_FILE = YES;
333 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
334 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
335 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
336 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
337 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
338 | LD_RUNPATH_SEARCH_PATHS = (
339 | "$(inherited)",
340 | "@executable_path/Frameworks",
341 | );
342 | MARKETING_VERSION = 1.0;
343 | PRODUCT_BUNDLE_IDENTIFIER = com.mayhem.MooCalTestApp;
344 | PRODUCT_NAME = "$(TARGET_NAME)";
345 | SWIFT_EMIT_LOC_STRINGS = YES;
346 | SWIFT_VERSION = 5.0;
347 | TARGETED_DEVICE_FAMILY = "1,2";
348 | };
349 | name = Debug;
350 | };
351 | 77D3E86B2B54DE6200DEAC1E /* Release */ = {
352 | isa = XCBuildConfiguration;
353 | buildSettings = {
354 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
355 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
356 | CODE_SIGN_STYLE = Automatic;
357 | CURRENT_PROJECT_VERSION = 1;
358 | DEVELOPMENT_ASSET_PATHS = "\"MooCalTestApp/Preview Content\"";
359 | DEVELOPMENT_TEAM = KPK2BX5PW8;
360 | ENABLE_PREVIEWS = YES;
361 | GENERATE_INFOPLIST_FILE = YES;
362 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
363 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
364 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
365 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
366 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
367 | LD_RUNPATH_SEARCH_PATHS = (
368 | "$(inherited)",
369 | "@executable_path/Frameworks",
370 | );
371 | MARKETING_VERSION = 1.0;
372 | PRODUCT_BUNDLE_IDENTIFIER = com.mayhem.MooCalTestApp;
373 | PRODUCT_NAME = "$(TARGET_NAME)";
374 | SWIFT_EMIT_LOC_STRINGS = YES;
375 | SWIFT_VERSION = 5.0;
376 | TARGETED_DEVICE_FAMILY = "1,2";
377 | };
378 | name = Release;
379 | };
380 | /* End XCBuildConfiguration section */
381 |
382 | /* Begin XCConfigurationList section */
383 | 77D3E8562B54DE5F00DEAC1E /* Build configuration list for PBXProject "MooCalTestApp" */ = {
384 | isa = XCConfigurationList;
385 | buildConfigurations = (
386 | 77D3E8672B54DE6200DEAC1E /* Debug */,
387 | 77D3E8682B54DE6200DEAC1E /* Release */,
388 | );
389 | defaultConfigurationIsVisible = 0;
390 | defaultConfigurationName = Release;
391 | };
392 | 77D3E8692B54DE6200DEAC1E /* Build configuration list for PBXNativeTarget "MooCalTestApp" */ = {
393 | isa = XCConfigurationList;
394 | buildConfigurations = (
395 | 77D3E86A2B54DE6200DEAC1E /* Debug */,
396 | 77D3E86B2B54DE6200DEAC1E /* Release */,
397 | );
398 | defaultConfigurationIsVisible = 0;
399 | defaultConfigurationName = Release;
400 | };
401 | /* End XCConfigurationList section */
402 |
403 | /* Begin XCSwiftPackageProductDependency section */
404 | 77D3E86F2B54E06700DEAC1E /* MooCal */ = {
405 | isa = XCSwiftPackageProductDependency;
406 | productName = MooCal;
407 | };
408 | /* End XCSwiftPackageProductDependency section */
409 | };
410 | rootObject = 77D3E8532B54DE5F00DEAC1E /* Project object */;
411 | }
412 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // MooCalTestApp
4 | //
5 | // Created by Colby Mehmen on 1/14/24.
6 | //
7 |
8 | import SwiftUI
9 | import MooCal
10 |
11 | struct ContentView: View {
12 | @State var events: [Event] = []
13 | @State var sheetState: SheetState? = nil
14 |
15 | var viewModel = ScrollableCalendarViewViewModel()
16 |
17 | var body: some View {
18 | ScrollViewReader { proxy in
19 | ScrollableCalendarView(
20 | viewModel: viewModel,
21 | calendarDayView: .custom({ calendarDay in
22 | customCalendarDayView(calendarDay)
23 | }),
24 | onSelection: { calendarDay in
25 | let events = events.filter({$0.date.isInDay(calendarDay.date)}) // filter to events in calendar day
26 | sheetState = .events(calendarDay, events)
27 | }
28 | )
29 | .toolbar {
30 | ToolbarItem(placement: .cancellationAction) {
31 | Button(action: {
32 | guard let currentMonthId = viewModel.currentMonthId else {
33 | return
34 | }
35 | proxy.scrollTo(currentMonthId)
36 | }, label: {
37 | Text("Today")
38 | .bold()
39 | })
40 | .tint(.orange)
41 | }
42 |
43 | ToolbarItem(placement: .confirmationAction) {
44 | Button {
45 | sheetState = .eventInputSheet
46 | } label: {
47 | Image(systemName: "plus.circle.fill")
48 | }
49 | .tint(.orange)
50 | }
51 | }
52 | .sheet(item: $sheetState, content: { sheetState in
53 | switch self.sheetState {
54 | case .eventInputSheet, .none:
55 | eventInputSheet()
56 | case .events(let calendarDay, let events):
57 | calendarDayEventView(calendarDay: calendarDay, events: events)
58 | }
59 | })
60 | }
61 | }
62 |
63 | // Custom View
64 | private func customCalendarDayView(_ calendarDay: CalendarDay) -> some View {
65 | let events = events.filter({$0.date.isInDay(calendarDay.date)})
66 | let isToday = calendarDay.date.isToday
67 |
68 | return ZStack {
69 | RoundedRectangle(cornerRadius: 10.0)
70 | .foregroundStyle((isToday) ? Color.orange : Color.gray)
71 | .opacity(0.33)
72 | Text(calendarDay.descriptor) // The day number. ex: 11
73 | .foregroundStyle(.white)
74 | .bold()
75 | }
76 | .aspectRatio(contentMode: .fit)
77 | .overlay {
78 | VStack {
79 | Spacer()
80 | HStack {
81 | ForEach(events) { event in
82 | Circle()
83 | .foregroundStyle((isToday) ? .blue: .orange)
84 | .frame(width: 5.0, height: 5.0)
85 | }
86 | Spacer()
87 | }
88 | .padding(6)
89 | }
90 | }
91 | }
92 |
93 | // View for inputting event info
94 | private func eventInputSheet() -> some View {
95 | NavigationView {
96 | EventInputSheet { newEvent in
97 | self.events.append(newEvent)
98 | }
99 | }
100 | .navigationTitle("New Event")
101 | }
102 |
103 | // View for viewing, updating and modifying events
104 | private func calendarDayEventView(calendarDay: CalendarDay, events: [Event]) -> some View {
105 | NavigationView {
106 | DayEventView(events: events) { action in
107 | switch action {
108 | case .delete(let event):
109 | guard let index = self.events.firstIndex(where: {$0.id == event.id}) else {
110 | return
111 | }
112 | self.events.remove(at: index)
113 |
114 | case .update(let event):
115 | guard let index = self.events.firstIndex(where: {$0.id == event.id}) else {
116 | return
117 | }
118 | self.events[index] = event
119 | }
120 | }
121 | .navigationTitle(calendarDay.date.formatted())
122 | }
123 | }
124 | }
125 |
126 | enum SheetState: Hashable, Identifiable {
127 | var id: Self { return self }
128 | case eventInputSheet
129 | case events(CalendarDay, [Event])
130 |
131 | var title: String {
132 | switch self {
133 | case .eventInputSheet:
134 | return "Event Input Sheet"
135 | case .events(_, _):
136 | return "Events"
137 | }
138 | }
139 |
140 | func hash(into hasher: inout Hasher) {
141 | hasher.combine(self.title)
142 |
143 | switch self {
144 | case .events(_, let events):
145 | hasher.combine(events)
146 | default:
147 | break
148 | }
149 | }
150 | }
151 |
152 | #Preview {
153 | NavigationView {
154 | ContentView()
155 | }
156 | }
157 |
158 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/ContentViewFour.swift:
--------------------------------------------------------------------------------
1 |
2 |
3 | //
4 | // SwiftUIView.swift
5 | //
6 | //
7 | // Created by Colby Mehmen on 5/27/24.
8 | //
9 |
10 | import SwiftUI
11 | import MooCal
12 |
13 | struct ContentViewFour: View {
14 | @State var displayMode: WeaatherDisplayMode = .icon
15 | @State var showingDisplaySelection: Bool = false
16 |
17 | var viewModel = ScrollableCalendarViewViewModel()
18 |
19 | var body: some View {
20 | ScrollableCalendarView(
21 | viewModel: viewModel,
22 | calendarDayView: .custom(
23 | { calendarDay in
24 | customCalendarDayView(calendarDay)
25 | }
26 | )
27 | )
28 | .sheet(isPresented: $showingDisplaySelection, content: {
29 | displayModeSelectionSheet()
30 | })
31 | .toolbar(content: {
32 | ToolbarItem {
33 | Button(action: {
34 | showingDisplaySelection.toggle()
35 | }, label: {
36 | self.displayMode.image
37 | .padding(6)
38 | .foregroundStyle(.white)
39 | .background {
40 | Circle()
41 | .foregroundStyle(Color.gray.opacity(0.33))
42 | }
43 | })
44 | }
45 | })
46 | }
47 |
48 | private func displayModeSelectionSheet() -> some View {
49 | List {
50 | ForEach(WeaatherDisplayMode.allCases) { displayMode in
51 | Label(
52 | title: { Text(displayMode.name) },
53 | icon: { displayMode.image }
54 | )
55 | .contentShape(Rectangle())
56 | .onTapGesture {
57 | self.displayMode = displayMode
58 | self.showingDisplaySelection = false
59 | }
60 | }
61 | }
62 | }
63 |
64 | // Custom Day View
65 | private func customCalendarDayView(_ calendarDay: CalendarDay) -> some View {
66 | return ZStack {
67 | VStack {
68 | Text(calendarDay.descriptor) // The day number. ex: 11
69 | .font(.system(.body, design: .rounded, weight: .bold))
70 | .foregroundStyle(.primary)
71 | .opacity(0.33)
72 |
73 | calendarDayContentView()
74 | .padding(.vertical, 1)
75 |
76 |
77 | }
78 | .padding(.bottom, 2)
79 | }
80 | .aspectRatio(contentMode: .fit)
81 | }
82 |
83 | @ViewBuilder
84 | private func calendarDayContentView() -> some View {
85 | switch self.displayMode {
86 |
87 | case .icon:
88 | WeatherTestAppUtil.randomWeatherType().image
89 | .font(.title2)
90 |
91 | case .temperature:
92 | let temp = WeatherTestAppUtil.randomTemperature()
93 |
94 | Text(WeatherTestAppUtil.tempFormattedString(temp: temp))
95 | .font(.title2)
96 | .foregroundStyle(Color.tempColor(value: temp))
97 | .bold()
98 |
99 | }
100 | }
101 | }
102 |
103 | // Util for mock data
104 | class WeatherTestAppUtil {
105 |
106 | static func randomWeatherType() -> WeatherType {
107 | let allWeatherTypes: [WeatherType] = [.sun, .rain, .heavyRain, .cloudy, .thunderStorm, .snow, .cloudyAndSunny, .windy]
108 | return allWeatherTypes.randomElement()!
109 | }
110 |
111 | static func randomTemperature() -> Int {
112 | return Int.random(in: 20...99)
113 | }
114 |
115 | static func tempFormattedString(temp: Int) -> String {
116 | return "\(temp)°"
117 | }
118 | }
119 |
120 | // Convenience extension for temp color
121 | extension Color {
122 | static func tempColor(value: Int) -> Color {
123 | if value < 40 {
124 | return .gray
125 | } else if value <= 75 {
126 | return .white
127 | } else if value <= 85 {
128 | return orange
129 | } else {
130 | return red
131 | }
132 | }
133 | }
134 |
135 | // Supported weather types
136 | enum WeatherType {
137 | case sun
138 | case rain
139 | case heavyRain
140 | case cloudy
141 | case thunderStorm
142 | case snow
143 | case cloudyAndSunny
144 | case windy
145 |
146 | @ViewBuilder
147 | var image: some View {
148 | switch self {
149 | case .sun:
150 | Image(systemName: "sun.max.fill")
151 | .foregroundStyle(.orange)
152 | .bold()
153 |
154 | case .rain:
155 | Image(systemName: "cloud.drizzle.fill")
156 | .symbolRenderingMode(.palette)
157 | .foregroundStyle(.white, .blue)
158 |
159 | case .heavyRain:
160 | Image(systemName: "cloud.sun.rain.fill")
161 | .symbolRenderingMode(.palette)
162 | .foregroundStyle(.white, .orange, .blue)
163 | case .cloudy:
164 | Image(systemName: "smoke.fill")
165 |
166 | case .thunderStorm:
167 | Image(systemName: "cloud.bolt.rain.fill")
168 | .symbolRenderingMode(.palette)
169 | .foregroundStyle(.white, .blue, .blue)
170 |
171 | case .snow:
172 | Image(systemName: "snowflake")
173 | .symbolRenderingMode(.palette)
174 | .foregroundStyle(.gray)
175 |
176 | case .cloudyAndSunny:
177 | Image(systemName: "cloud.sun.fill")
178 | .symbolRenderingMode(.palette)
179 | .foregroundStyle(.white, .orange)
180 |
181 | case .windy:
182 | Image(systemName: "wind")
183 | }
184 | }
185 | }
186 |
187 | // Display Modes for calendar
188 | enum WeaatherDisplayMode: String, CaseIterable, Identifiable {
189 | var id: Self { return self }
190 |
191 | case icon
192 | case temperature
193 |
194 | var name: String {
195 | switch self {
196 | case .icon:
197 | return "Icons"
198 | case .temperature:
199 | return "Temperature"
200 | }
201 | }
202 |
203 | var image: Image {
204 | switch self {
205 | case .icon:
206 | Image(systemName: "sun.max.fill")
207 | case .temperature:
208 | Image(systemName: "thermometer.high")
209 | }
210 | }
211 | }
212 |
213 | #Preview {
214 | NavigationView {
215 | ContentViewFour()
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/ContentViewThree.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentViewThree.swift
3 | // MooCalTestApp
4 | //
5 | // Created by Colby Mehmen on 1/15/24.
6 | //
7 |
8 | import SwiftUI
9 | import MooCal
10 |
11 | struct ContentViewThree: View {
12 | @State var events: [Event] = []
13 | @State var sheetState: SheetState? = nil
14 |
15 | var viewModel = ScrollableCalendarViewViewModel()
16 |
17 | var body: some View {
18 | ScrollViewReader { proxy in
19 | ScrollableCalendarView(
20 | viewModel: viewModel,
21 | calendarDayView: .custom(
22 | { calendarDay in
23 | customCalendarDayView(calendarDay)
24 | }
25 | ),
26 | calendarDayOfWeekInidcatorView: .custom({ dayOfWeek in
27 | customDayOfWeekIndicatorView(dayOfWeek: dayOfWeek)
28 | })
29 | )
30 | .toolbar {
31 | ToolbarItem(placement: .cancellationAction) {
32 | Button(action: {
33 | guard let currentMonthId = viewModel.currentMonthId else {
34 | return
35 | }
36 | proxy.scrollTo(currentMonthId)
37 | }, label: {
38 | Text("Today")
39 | .bold()
40 | })
41 | .tint(Color.blue)
42 | }
43 |
44 | ToolbarItem(placement: .confirmationAction) {
45 | Button {
46 | sheetState = .eventInputSheet
47 | } label: {
48 | Image(systemName: "plus.circle.fill")
49 | }
50 | .tint(Color.blue)
51 | }
52 | }
53 | .sheet(item: $sheetState, content: { sheetState in
54 | switch self.sheetState {
55 | case .eventInputSheet, .none:
56 | eventInputSheet()
57 | case .events(let calendarDay, let events):
58 | calendarDayEventView(calendarDay: calendarDay, events: events)
59 | }
60 | })
61 | }
62 | }
63 |
64 | // Custom Day View
65 | private func customCalendarDayView(_ calendarDay: CalendarDay) -> some View {
66 | let events = events.filter({$0.date.isInDay(calendarDay.date)})
67 | let isToday = calendarDay.date.isToday
68 |
69 | return ZStack {
70 | Circle()
71 | .foregroundStyle((isToday) ? Color.green: Color.gray)
72 | .opacity(0.33)
73 | Text(calendarDay.descriptor) // The day number. ex: 11
74 | .font(.system(.body, design: .rounded, weight: .bold))
75 | .foregroundStyle(.primary)
76 | }
77 | .aspectRatio(contentMode: .fit)
78 | .overlay {
79 | VStack {
80 | Spacer()
81 | HStack {
82 | ForEach(events) { event in
83 | Circle()
84 | .foregroundStyle((isToday) ? .pink: .orange)
85 | .frame(width: 5.0, height: 5.0)
86 | }
87 | Spacer()
88 | }
89 | .padding(6)
90 | }
91 | }
92 | }
93 |
94 | // Custom Day Of Week Indicator View
95 | private func customDayOfWeekIndicatorView(dayOfWeek: DayOfWeek) -> some View {
96 | ZStack {
97 | Text(dayOfWeek.abbreviated)
98 | .font(.system(.body, design: .rounded, weight: .semibold))
99 | }
100 | }
101 |
102 |
103 | // View for inputting event info
104 | private func eventInputSheet() -> some View {
105 | NavigationView {
106 | EventInputSheet { newEvent in
107 | self.events.append(newEvent)
108 | }
109 | }
110 | .navigationTitle("New Event")
111 | }
112 |
113 | // View for viewing, updating and modifying events
114 | private func calendarDayEventView(calendarDay: CalendarDay, events: [Event]) -> some View {
115 | NavigationView {
116 | DayEventView(events: events) { action in
117 | switch action {
118 | case .delete(let event):
119 | guard let index = self.events.firstIndex(where: {$0.id == event.id}) else {
120 | return
121 | }
122 | self.events.remove(at: index)
123 |
124 | case .update(let event):
125 | guard let index = self.events.firstIndex(where: {$0.id == event.id}) else {
126 | return
127 | }
128 | self.events[index] = event
129 | }
130 | }
131 | .navigationTitle(calendarDay.date.formatted())
132 | }
133 | }
134 | }
135 |
136 | extension ContentViewThree {
137 | enum SheetState: Hashable, Identifiable {
138 | var id: Self { return self }
139 | case eventInputSheet
140 | case events(CalendarDay, [Event])
141 |
142 | var title: String {
143 | switch self {
144 | case .eventInputSheet:
145 | return "Event Input Sheet"
146 | case .events(_, _):
147 | return "Events"
148 | }
149 | }
150 |
151 | func hash(into hasher: inout Hasher) {
152 | hasher.combine(self.title)
153 |
154 | switch self {
155 | case .events(_, let events):
156 | hasher.combine(events)
157 | default:
158 | break
159 | }
160 | }
161 | }
162 | }
163 |
164 | #Preview {
165 | NavigationView {
166 | ContentViewThree()
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/ContentViewTwo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentViewTwo.swift
3 | // MooCalTestApp
4 | //
5 | // Created by Colby Mehmen on 1/15/24.
6 | //
7 |
8 | import SwiftUI
9 | import MooCal
10 |
11 | struct ContentViewTwo: View {
12 | @State var events: [Event] = []
13 | @State var sheetState: SheetState? = nil
14 |
15 | var viewModel = ScrollableCalendarViewViewModel()
16 |
17 | var body: some View {
18 | ScrollViewReader { proxy in
19 | ScrollableCalendarView(
20 | viewModel: viewModel,
21 | calendarHeaderView: .custom({ calendarMonth in
22 | HStack {
23 | Spacer()
24 | Text(calendarMonth.title)
25 | .font(.system(.title2, design: .rounded, weight: .bold))
26 | Spacer()
27 | }
28 | .padding(.leading)
29 | }),
30 | calendarDayView: .custom({ calendarDay in
31 | customCalendarDayView(calendarDay)
32 | })
33 | )
34 | .toolbar {
35 | ToolbarItem(placement: .cancellationAction) {
36 | Button(action: {
37 | guard let currentMonthId = viewModel.currentMonthId else {
38 | return
39 | }
40 | proxy.scrollTo(currentMonthId)
41 | }, label: {
42 | Text("Today")
43 | .bold()
44 | })
45 | .tint(Color.blue)
46 | }
47 |
48 | ToolbarItem(placement: .confirmationAction) {
49 | Button {
50 | sheetState = .eventInputSheet
51 | } label: {
52 | Image(systemName: "plus.circle.fill")
53 | }
54 | .tint(Color.blue)
55 | }
56 | }
57 | .sheet(item: $sheetState, content: { sheetState in
58 | switch self.sheetState {
59 | case .eventInputSheet, .none:
60 | eventInputSheet()
61 | case .events(let calendarDay, let events):
62 | calendarDayEventView(calendarDay: calendarDay, events: events)
63 | }
64 | })
65 | }
66 | }
67 |
68 | // Custom View
69 | private func customCalendarDayView(_ calendarDay: CalendarDay) -> some View {
70 | let events = events.filter({$0.date.isInDay(calendarDay.date)})
71 | let isToday = calendarDay.date.isToday
72 |
73 | return ZStack {
74 | Circle()
75 | .foregroundStyle((isToday) ? Color.blue.opacity(0.33) : Color.gray.opacity(0.13))
76 | Text(calendarDay.descriptor) // The day number. ex: 11
77 | .foregroundStyle(.primary.opacity(isToday ? 1.0 : 0.5))
78 | .bold()
79 | }
80 | .aspectRatio(contentMode: .fit)
81 | .overlay {
82 | VStack {
83 | Spacer()
84 | HStack {
85 | ForEach(events) { event in
86 | Circle()
87 | .foregroundStyle((isToday) ? .cyan : .orange)
88 | .frame(width: 5.0, height: 5.0)
89 | }
90 | Spacer()
91 | }
92 | .padding(6)
93 | }
94 | }
95 | }
96 |
97 | // View for inputting event info
98 | private func eventInputSheet() -> some View {
99 | NavigationView {
100 | EventInputSheet { newEvent in
101 | self.events.append(newEvent)
102 | }
103 | }
104 | .navigationTitle("New Event")
105 | }
106 |
107 | // View for viewing, updating and modifying events
108 | private func calendarDayEventView(calendarDay: CalendarDay, events: [Event]) -> some View {
109 | NavigationView {
110 | DayEventView(events: events) { action in
111 | switch action {
112 | case .delete(let event):
113 | guard let index = self.events.firstIndex(where: {$0.id == event.id}) else {
114 | return
115 | }
116 | self.events.remove(at: index)
117 |
118 | case .update(let event):
119 | guard let index = self.events.firstIndex(where: {$0.id == event.id}) else {
120 | return
121 | }
122 | self.events[index] = event
123 | }
124 | }
125 | .navigationTitle(calendarDay.date.formatted())
126 | }
127 | }
128 | }
129 |
130 | extension ContentViewTwo {
131 | enum SheetState: Hashable, Identifiable {
132 | var id: Self { return self }
133 | case eventInputSheet
134 | case events(CalendarDay, [Event])
135 |
136 | var title: String {
137 | switch self {
138 | case .eventInputSheet:
139 | return "Event Input Sheet"
140 | case .events(_, _):
141 | return "Events"
142 | }
143 | }
144 |
145 | func hash(into hasher: inout Hasher) {
146 | hasher.combine(self.title)
147 |
148 | switch self {
149 | case .events(_, let events):
150 | hasher.combine(events)
151 | default:
152 | break
153 | }
154 | }
155 | }
156 | }
157 |
158 | #Preview {
159 | NavigationView {
160 | ContentViewTwo()
161 | }
162 | }
163 |
164 |
165 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/DataModels/Event.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Event.swift
3 | // MooCalTestApp
4 | //
5 | // Created by Colby Mehmen on 1/14/24.
6 | //
7 |
8 | import SwiftUI
9 | import MooCal
10 |
11 | public struct Event: ClassicCalendarData, CalendarData, Identifiable {
12 | public var id: UUID
13 | public var date: Date
14 | public var color: Color
15 | public var title: String
16 | public var creator: String
17 |
18 | public init(id: UUID = UUID(), date: Date, color: Color, title: String, creator: String) {
19 | self.id = id
20 | self.date = date
21 | self.color = color
22 | self.title = title
23 | self.creator = creator
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/MooCalTestAppApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MooCalTestAppApp.swift
3 | // MooCalTestApp
4 | //
5 | // Created by Colby Mehmen on 1/14/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct MooCalTestAppApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | TabView {
15 | NavigationView {
16 | ContentView()
17 | }
18 | .tabItem {
19 | Image(systemName: "1.circle.fill")
20 | }
21 |
22 | NavigationView {
23 | ContentViewTwo()
24 | }
25 | .tabItem {
26 | Image(systemName: "2.circle.fill")
27 | }
28 |
29 | NavigationView {
30 | ContentViewThree()
31 | }
32 | .tabItem {
33 | Image(systemName: "3.circle.fill")
34 | }
35 |
36 | NavigationView {
37 | ContentViewFour()
38 | }
39 | .tabItem {
40 | Image(systemName: "4.circle.fill")
41 | }
42 | }
43 |
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/Views/DayEventView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DayEventView.swift
3 | // MooCalTestApp
4 | //
5 | // Created by Colby Mehmen on 1/14/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DayEventView: View {
11 | @State var events: [Event]
12 |
13 | var onAction: (Action) -> ()
14 |
15 | enum Action {
16 | case delete(Event)
17 | case update(Event)
18 | }
19 |
20 | init(events: [Event], onAction: @escaping (Action) -> ()) {
21 | self._events = State(initialValue: events)
22 | self.onAction = onAction
23 | }
24 |
25 | var body: some View {
26 | List {
27 | ForEach(events) { event in
28 | NavigationLink {
29 | EventInputSheet(event: event) { event in
30 | update(event: event)
31 | }
32 | } label: {
33 | eventRowItemView(event: event)
34 | }
35 | .swipeActions {
36 | Button(action: {
37 | delete(event: event)
38 | }, label: {
39 | Image(systemName: "trash.fill")
40 | })
41 | .tint(Color.red)
42 | }
43 |
44 | }
45 | }
46 | }
47 |
48 | func eventRowItemView(event: Event) -> some View {
49 | VStack {
50 | HStack {
51 | Circle()
52 | .frame(width: 10.0, height: 10.0)
53 | Text(event.title)
54 | Spacer()
55 | }
56 | HStack {
57 | Spacer()
58 | Text(event.date.formatted())
59 | .font(.caption)
60 | }
61 | }
62 | }
63 |
64 | func delete(event: Event) {
65 | guard let index = events.firstIndex(of: event) else {
66 | return
67 | }
68 | self.events.remove(at: index)
69 | self.onAction(.delete(event))
70 | }
71 |
72 | func update(event: Event) {
73 | guard let index = events.firstIndex(where: {$0.id == event.id}) else {
74 | return
75 | }
76 | self.events[index] = event
77 | self.onAction(.update(event))
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/MooCalTestApp/MooCalTestApp/Views/EventInputSheet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EventInputSheet.swift
3 | // MooCalTestApp
4 | //
5 | // Created by Colby Mehmen on 1/14/24.
6 | //
7 |
8 | import SwiftUI
9 | import MooCal
10 |
11 | struct EventInputSheet: View {
12 | @Environment(\.dismiss) var dismiss
13 |
14 | private var id: UUID
15 |
16 | @State private var date: Date
17 | @State private var color: Color
18 | @State private var title: String
19 | @State private var creator: String
20 |
21 | var onSave: (Event) -> Void
22 |
23 | init(id: UUID = UUID(), date: Date = Date(), color: SwiftUI.Color = Color.blue, title: String = "", creator: String = "", onSave: @escaping (Event) -> Void) {
24 | self.id = id
25 | self._date = State(initialValue: date)
26 | self._color = State(initialValue: color)
27 | self._title = State(initialValue: title)
28 | self._creator = State(initialValue: creator)
29 | self.onSave = onSave
30 | }
31 |
32 | init(event: Event, onSave: @escaping (Event) -> Void) {
33 | self.init(
34 | id: event.id,
35 | date: event.date,
36 | color: event.color,
37 | title: event.title,
38 | creator: event.creator,
39 | onSave: onSave)
40 | }
41 |
42 | var body: some View {
43 | Form {
44 | DatePicker("Date", selection: $date, displayedComponents: .date)
45 | ColorPicker("Color", selection: $color)
46 | TextField("Title", text: $title)
47 | TextField("Creator", text: $creator)
48 |
49 | Button("Save") {
50 | let event = Event(
51 | id: id,
52 | date: date,
53 | color: color,
54 | title: title,
55 | creator: creator
56 | )
57 |
58 | onSave(event)
59 | dismiss()
60 | }
61 | }
62 | .navigationTitle("New Event")
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Tests/MooCalTests/MooCalTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import MooCal
3 |
4 | final class MooCalTests: XCTestCase {
5 | func testExample() throws {
6 | // XCTest Documentation
7 | // https://developer.apple.com/documentation/xctest
8 |
9 | // Defining Test Cases and Test Methods
10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
11 | }
12 | }
13 |
--------------------------------------------------------------------------------