├── .gitignore ├── LICENSE ├── MultiDatePickerApp.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── MultiDatePickerApp ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── Info.plist ├── MultiDatePicker Source │ ├── Components │ │ ├── MDPContentView.swift │ │ ├── MDPDayOfMonth.swift │ │ ├── MDPDayView.swift │ │ ├── MDPModel.swift │ │ ├── MDPMonthView.swift │ │ ├── MDPMonthYearPicker.swift │ │ └── MDPMonthYearPickerButton.swift │ └── MultiDatePicker.swift ├── MultiDatePickerAppApp.swift └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── MultiDatePickerAppTests ├── Info.plist └── MultiDatePickerAppTests.swift ├── MultiDatePickerAppUITests ├── Info.plist └── MultiDatePickerAppUITests.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright 2021 Peter Ent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /MultiDatePickerApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 321AB153255049EA00CE3EFC /* MultiDatePickerAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 321AB152255049EA00CE3EFC /* MultiDatePickerAppApp.swift */; }; 11 | 321AB155255049EA00CE3EFC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 321AB154255049EA00CE3EFC /* ContentView.swift */; }; 12 | 321AB157255049EA00CE3EFC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 321AB156255049EA00CE3EFC /* Assets.xcassets */; }; 13 | 321AB15A255049EA00CE3EFC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 321AB159255049EA00CE3EFC /* Preview Assets.xcassets */; }; 14 | 321AB165255049EA00CE3EFC /* MultiDatePickerAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 321AB164255049EA00CE3EFC /* MultiDatePickerAppTests.swift */; }; 15 | 321AB170255049EA00CE3EFC /* MultiDatePickerAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 321AB16F255049EA00CE3EFC /* MultiDatePickerAppUITests.swift */; }; 16 | 321AB18125504FB100CE3EFC /* MDPModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 321AB18025504FB100CE3EFC /* MDPModel.swift */; }; 17 | 321AB18625504FDE00CE3EFC /* MDPDayOfMonth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 321AB18525504FDE00CE3EFC /* MDPDayOfMonth.swift */; }; 18 | 326C9FF1255058FD0054B8F1 /* MDPMonthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326C9FF0255058FD0054B8F1 /* MDPMonthView.swift */; }; 19 | 326C9FF625507C920054B8F1 /* MDPMonthYearPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326C9FF525507C920054B8F1 /* MDPMonthYearPicker.swift */; }; 20 | 326C9FFB2550B5A50054B8F1 /* MDPDayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326C9FFA2550B5A50054B8F1 /* MDPDayView.swift */; }; 21 | 3279D68825518F9F003D737D /* MDPContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3279D68725518F9F003D737D /* MDPContentView.swift */; }; 22 | 3279D68D2551918A003D737D /* MDPMonthYearPickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3279D68C2551918A003D737D /* MDPMonthYearPickerButton.swift */; }; 23 | 3279D69725519604003D737D /* MultiDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3279D69625519604003D737D /* MultiDatePicker.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | 321AB161255049EA00CE3EFC /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = 321AB147255049E900CE3EFC /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = 321AB14E255049EA00CE3EFC; 32 | remoteInfo = MultiDatePickerApp; 33 | }; 34 | 321AB16C255049EA00CE3EFC /* PBXContainerItemProxy */ = { 35 | isa = PBXContainerItemProxy; 36 | containerPortal = 321AB147255049E900CE3EFC /* Project object */; 37 | proxyType = 1; 38 | remoteGlobalIDString = 321AB14E255049EA00CE3EFC; 39 | remoteInfo = MultiDatePickerApp; 40 | }; 41 | /* End PBXContainerItemProxy section */ 42 | 43 | /* Begin PBXFileReference section */ 44 | 321AB14F255049EA00CE3EFC /* MultiDatePickerApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MultiDatePickerApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 321AB152255049EA00CE3EFC /* MultiDatePickerAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiDatePickerAppApp.swift; sourceTree = ""; }; 46 | 321AB154255049EA00CE3EFC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 47 | 321AB156255049EA00CE3EFC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 321AB159255049EA00CE3EFC /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 49 | 321AB15B255049EA00CE3EFC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | 321AB160255049EA00CE3EFC /* MultiDatePickerAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MultiDatePickerAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 321AB164255049EA00CE3EFC /* MultiDatePickerAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiDatePickerAppTests.swift; sourceTree = ""; }; 52 | 321AB166255049EA00CE3EFC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | 321AB16B255049EA00CE3EFC /* MultiDatePickerAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MultiDatePickerAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 321AB16F255049EA00CE3EFC /* MultiDatePickerAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiDatePickerAppUITests.swift; sourceTree = ""; }; 55 | 321AB171255049EA00CE3EFC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 321AB18025504FB100CE3EFC /* MDPModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDPModel.swift; sourceTree = ""; }; 57 | 321AB18525504FDE00CE3EFC /* MDPDayOfMonth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDPDayOfMonth.swift; sourceTree = ""; }; 58 | 326C9FF0255058FD0054B8F1 /* MDPMonthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDPMonthView.swift; sourceTree = ""; }; 59 | 326C9FF525507C920054B8F1 /* MDPMonthYearPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDPMonthYearPicker.swift; sourceTree = ""; }; 60 | 326C9FFA2550B5A50054B8F1 /* MDPDayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDPDayView.swift; sourceTree = ""; }; 61 | 3279D68725518F9F003D737D /* MDPContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDPContentView.swift; sourceTree = ""; }; 62 | 3279D68C2551918A003D737D /* MDPMonthYearPickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDPMonthYearPickerButton.swift; sourceTree = ""; }; 63 | 3279D69625519604003D737D /* MultiDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiDatePicker.swift; sourceTree = ""; }; 64 | /* End PBXFileReference section */ 65 | 66 | /* Begin PBXFrameworksBuildPhase section */ 67 | 321AB14C255049EA00CE3EFC /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | 321AB15D255049EA00CE3EFC /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | 321AB168255049EA00CE3EFC /* Frameworks */ = { 82 | isa = PBXFrameworksBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | /* End PBXFrameworksBuildPhase section */ 89 | 90 | /* Begin PBXGroup section */ 91 | 321AB146255049E900CE3EFC = { 92 | isa = PBXGroup; 93 | children = ( 94 | 321AB151255049EA00CE3EFC /* MultiDatePickerApp */, 95 | 321AB163255049EA00CE3EFC /* MultiDatePickerAppTests */, 96 | 321AB16E255049EA00CE3EFC /* MultiDatePickerAppUITests */, 97 | 321AB150255049EA00CE3EFC /* Products */, 98 | ); 99 | sourceTree = ""; 100 | }; 101 | 321AB150255049EA00CE3EFC /* Products */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 321AB14F255049EA00CE3EFC /* MultiDatePickerApp.app */, 105 | 321AB160255049EA00CE3EFC /* MultiDatePickerAppTests.xctest */, 106 | 321AB16B255049EA00CE3EFC /* MultiDatePickerAppUITests.xctest */, 107 | ); 108 | name = Products; 109 | sourceTree = ""; 110 | }; 111 | 321AB151255049EA00CE3EFC /* MultiDatePickerApp */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 321AB152255049EA00CE3EFC /* MultiDatePickerAppApp.swift */, 115 | 321AB154255049EA00CE3EFC /* ContentView.swift */, 116 | 327F233C25532A7D00AAA529 /* MultiDatePicker Source */, 117 | 321AB156255049EA00CE3EFC /* Assets.xcassets */, 118 | 321AB15B255049EA00CE3EFC /* Info.plist */, 119 | 321AB158255049EA00CE3EFC /* Preview Content */, 120 | ); 121 | path = MultiDatePickerApp; 122 | sourceTree = ""; 123 | }; 124 | 321AB158255049EA00CE3EFC /* Preview Content */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 321AB159255049EA00CE3EFC /* Preview Assets.xcassets */, 128 | ); 129 | path = "Preview Content"; 130 | sourceTree = ""; 131 | }; 132 | 321AB163255049EA00CE3EFC /* MultiDatePickerAppTests */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 321AB164255049EA00CE3EFC /* MultiDatePickerAppTests.swift */, 136 | 321AB166255049EA00CE3EFC /* Info.plist */, 137 | ); 138 | path = MultiDatePickerAppTests; 139 | sourceTree = ""; 140 | }; 141 | 321AB16E255049EA00CE3EFC /* MultiDatePickerAppUITests */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 321AB16F255049EA00CE3EFC /* MultiDatePickerAppUITests.swift */, 145 | 321AB171255049EA00CE3EFC /* Info.plist */, 146 | ); 147 | path = MultiDatePickerAppUITests; 148 | sourceTree = ""; 149 | }; 150 | 327F233C25532A7D00AAA529 /* MultiDatePicker Source */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 3279D69625519604003D737D /* MultiDatePicker.swift */, 154 | 327F234025532A9400AAA529 /* Components */, 155 | ); 156 | path = "MultiDatePicker Source"; 157 | sourceTree = ""; 158 | }; 159 | 327F234025532A9400AAA529 /* Components */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 321AB18025504FB100CE3EFC /* MDPModel.swift */, 163 | 321AB18525504FDE00CE3EFC /* MDPDayOfMonth.swift */, 164 | 326C9FF0255058FD0054B8F1 /* MDPMonthView.swift */, 165 | 3279D68725518F9F003D737D /* MDPContentView.swift */, 166 | 326C9FFA2550B5A50054B8F1 /* MDPDayView.swift */, 167 | 326C9FF525507C920054B8F1 /* MDPMonthYearPicker.swift */, 168 | 3279D68C2551918A003D737D /* MDPMonthYearPickerButton.swift */, 169 | ); 170 | path = Components; 171 | sourceTree = ""; 172 | }; 173 | /* End PBXGroup section */ 174 | 175 | /* Begin PBXNativeTarget section */ 176 | 321AB14E255049EA00CE3EFC /* MultiDatePickerApp */ = { 177 | isa = PBXNativeTarget; 178 | buildConfigurationList = 321AB174255049EA00CE3EFC /* Build configuration list for PBXNativeTarget "MultiDatePickerApp" */; 179 | buildPhases = ( 180 | 321AB14B255049EA00CE3EFC /* Sources */, 181 | 321AB14C255049EA00CE3EFC /* Frameworks */, 182 | 321AB14D255049EA00CE3EFC /* Resources */, 183 | ); 184 | buildRules = ( 185 | ); 186 | dependencies = ( 187 | ); 188 | name = MultiDatePickerApp; 189 | productName = MultiDatePickerApp; 190 | productReference = 321AB14F255049EA00CE3EFC /* MultiDatePickerApp.app */; 191 | productType = "com.apple.product-type.application"; 192 | }; 193 | 321AB15F255049EA00CE3EFC /* MultiDatePickerAppTests */ = { 194 | isa = PBXNativeTarget; 195 | buildConfigurationList = 321AB177255049EA00CE3EFC /* Build configuration list for PBXNativeTarget "MultiDatePickerAppTests" */; 196 | buildPhases = ( 197 | 321AB15C255049EA00CE3EFC /* Sources */, 198 | 321AB15D255049EA00CE3EFC /* Frameworks */, 199 | 321AB15E255049EA00CE3EFC /* Resources */, 200 | ); 201 | buildRules = ( 202 | ); 203 | dependencies = ( 204 | 321AB162255049EA00CE3EFC /* PBXTargetDependency */, 205 | ); 206 | name = MultiDatePickerAppTests; 207 | productName = MultiDatePickerAppTests; 208 | productReference = 321AB160255049EA00CE3EFC /* MultiDatePickerAppTests.xctest */; 209 | productType = "com.apple.product-type.bundle.unit-test"; 210 | }; 211 | 321AB16A255049EA00CE3EFC /* MultiDatePickerAppUITests */ = { 212 | isa = PBXNativeTarget; 213 | buildConfigurationList = 321AB17A255049EA00CE3EFC /* Build configuration list for PBXNativeTarget "MultiDatePickerAppUITests" */; 214 | buildPhases = ( 215 | 321AB167255049EA00CE3EFC /* Sources */, 216 | 321AB168255049EA00CE3EFC /* Frameworks */, 217 | 321AB169255049EA00CE3EFC /* Resources */, 218 | ); 219 | buildRules = ( 220 | ); 221 | dependencies = ( 222 | 321AB16D255049EA00CE3EFC /* PBXTargetDependency */, 223 | ); 224 | name = MultiDatePickerAppUITests; 225 | productName = MultiDatePickerAppUITests; 226 | productReference = 321AB16B255049EA00CE3EFC /* MultiDatePickerAppUITests.xctest */; 227 | productType = "com.apple.product-type.bundle.ui-testing"; 228 | }; 229 | /* End PBXNativeTarget section */ 230 | 231 | /* Begin PBXProject section */ 232 | 321AB147255049E900CE3EFC /* Project object */ = { 233 | isa = PBXProject; 234 | attributes = { 235 | LastSwiftUpdateCheck = 1210; 236 | LastUpgradeCheck = 1210; 237 | TargetAttributes = { 238 | 321AB14E255049EA00CE3EFC = { 239 | CreatedOnToolsVersion = 12.1; 240 | }; 241 | 321AB15F255049EA00CE3EFC = { 242 | CreatedOnToolsVersion = 12.1; 243 | TestTargetID = 321AB14E255049EA00CE3EFC; 244 | }; 245 | 321AB16A255049EA00CE3EFC = { 246 | CreatedOnToolsVersion = 12.1; 247 | TestTargetID = 321AB14E255049EA00CE3EFC; 248 | }; 249 | }; 250 | }; 251 | buildConfigurationList = 321AB14A255049E900CE3EFC /* Build configuration list for PBXProject "MultiDatePickerApp" */; 252 | compatibilityVersion = "Xcode 9.3"; 253 | developmentRegion = en; 254 | hasScannedForEncodings = 0; 255 | knownRegions = ( 256 | en, 257 | Base, 258 | ); 259 | mainGroup = 321AB146255049E900CE3EFC; 260 | productRefGroup = 321AB150255049EA00CE3EFC /* Products */; 261 | projectDirPath = ""; 262 | projectRoot = ""; 263 | targets = ( 264 | 321AB14E255049EA00CE3EFC /* MultiDatePickerApp */, 265 | 321AB15F255049EA00CE3EFC /* MultiDatePickerAppTests */, 266 | 321AB16A255049EA00CE3EFC /* MultiDatePickerAppUITests */, 267 | ); 268 | }; 269 | /* End PBXProject section */ 270 | 271 | /* Begin PBXResourcesBuildPhase section */ 272 | 321AB14D255049EA00CE3EFC /* Resources */ = { 273 | isa = PBXResourcesBuildPhase; 274 | buildActionMask = 2147483647; 275 | files = ( 276 | 321AB15A255049EA00CE3EFC /* Preview Assets.xcassets in Resources */, 277 | 321AB157255049EA00CE3EFC /* Assets.xcassets in Resources */, 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | 321AB15E255049EA00CE3EFC /* Resources */ = { 282 | isa = PBXResourcesBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | ); 286 | runOnlyForDeploymentPostprocessing = 0; 287 | }; 288 | 321AB169255049EA00CE3EFC /* Resources */ = { 289 | isa = PBXResourcesBuildPhase; 290 | buildActionMask = 2147483647; 291 | files = ( 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | }; 295 | /* End PBXResourcesBuildPhase section */ 296 | 297 | /* Begin PBXSourcesBuildPhase section */ 298 | 321AB14B255049EA00CE3EFC /* Sources */ = { 299 | isa = PBXSourcesBuildPhase; 300 | buildActionMask = 2147483647; 301 | files = ( 302 | 321AB18625504FDE00CE3EFC /* MDPDayOfMonth.swift in Sources */, 303 | 326C9FF1255058FD0054B8F1 /* MDPMonthView.swift in Sources */, 304 | 321AB18125504FB100CE3EFC /* MDPModel.swift in Sources */, 305 | 326C9FF625507C920054B8F1 /* MDPMonthYearPicker.swift in Sources */, 306 | 326C9FFB2550B5A50054B8F1 /* MDPDayView.swift in Sources */, 307 | 3279D68D2551918A003D737D /* MDPMonthYearPickerButton.swift in Sources */, 308 | 3279D68825518F9F003D737D /* MDPContentView.swift in Sources */, 309 | 321AB155255049EA00CE3EFC /* ContentView.swift in Sources */, 310 | 3279D69725519604003D737D /* MultiDatePicker.swift in Sources */, 311 | 321AB153255049EA00CE3EFC /* MultiDatePickerAppApp.swift in Sources */, 312 | ); 313 | runOnlyForDeploymentPostprocessing = 0; 314 | }; 315 | 321AB15C255049EA00CE3EFC /* Sources */ = { 316 | isa = PBXSourcesBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | 321AB165255049EA00CE3EFC /* MultiDatePickerAppTests.swift in Sources */, 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | }; 323 | 321AB167255049EA00CE3EFC /* Sources */ = { 324 | isa = PBXSourcesBuildPhase; 325 | buildActionMask = 2147483647; 326 | files = ( 327 | 321AB170255049EA00CE3EFC /* MultiDatePickerAppUITests.swift in Sources */, 328 | ); 329 | runOnlyForDeploymentPostprocessing = 0; 330 | }; 331 | /* End PBXSourcesBuildPhase section */ 332 | 333 | /* Begin PBXTargetDependency section */ 334 | 321AB162255049EA00CE3EFC /* PBXTargetDependency */ = { 335 | isa = PBXTargetDependency; 336 | target = 321AB14E255049EA00CE3EFC /* MultiDatePickerApp */; 337 | targetProxy = 321AB161255049EA00CE3EFC /* PBXContainerItemProxy */; 338 | }; 339 | 321AB16D255049EA00CE3EFC /* PBXTargetDependency */ = { 340 | isa = PBXTargetDependency; 341 | target = 321AB14E255049EA00CE3EFC /* MultiDatePickerApp */; 342 | targetProxy = 321AB16C255049EA00CE3EFC /* PBXContainerItemProxy */; 343 | }; 344 | /* End PBXTargetDependency section */ 345 | 346 | /* Begin XCBuildConfiguration section */ 347 | 321AB172255049EA00CE3EFC /* Debug */ = { 348 | isa = XCBuildConfiguration; 349 | buildSettings = { 350 | ALWAYS_SEARCH_USER_PATHS = NO; 351 | CLANG_ANALYZER_NONNULL = YES; 352 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 353 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 354 | CLANG_CXX_LIBRARY = "libc++"; 355 | CLANG_ENABLE_MODULES = YES; 356 | CLANG_ENABLE_OBJC_ARC = YES; 357 | CLANG_ENABLE_OBJC_WEAK = YES; 358 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 359 | CLANG_WARN_BOOL_CONVERSION = YES; 360 | CLANG_WARN_COMMA = YES; 361 | CLANG_WARN_CONSTANT_CONVERSION = YES; 362 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 363 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 364 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 365 | CLANG_WARN_EMPTY_BODY = YES; 366 | CLANG_WARN_ENUM_CONVERSION = YES; 367 | CLANG_WARN_INFINITE_RECURSION = YES; 368 | CLANG_WARN_INT_CONVERSION = YES; 369 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 370 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 371 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 372 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 373 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 374 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 375 | CLANG_WARN_STRICT_PROTOTYPES = YES; 376 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 377 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 378 | CLANG_WARN_UNREACHABLE_CODE = YES; 379 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 380 | COPY_PHASE_STRIP = NO; 381 | DEBUG_INFORMATION_FORMAT = dwarf; 382 | ENABLE_STRICT_OBJC_MSGSEND = YES; 383 | ENABLE_TESTABILITY = YES; 384 | GCC_C_LANGUAGE_STANDARD = gnu11; 385 | GCC_DYNAMIC_NO_PIC = NO; 386 | GCC_NO_COMMON_BLOCKS = YES; 387 | GCC_OPTIMIZATION_LEVEL = 0; 388 | GCC_PREPROCESSOR_DEFINITIONS = ( 389 | "DEBUG=1", 390 | "$(inherited)", 391 | ); 392 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 393 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 394 | GCC_WARN_UNDECLARED_SELECTOR = YES; 395 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 396 | GCC_WARN_UNUSED_FUNCTION = YES; 397 | GCC_WARN_UNUSED_VARIABLE = YES; 398 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 399 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 400 | MTL_FAST_MATH = YES; 401 | ONLY_ACTIVE_ARCH = YES; 402 | SDKROOT = iphoneos; 403 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 404 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 405 | }; 406 | name = Debug; 407 | }; 408 | 321AB173255049EA00CE3EFC /* Release */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | ALWAYS_SEARCH_USER_PATHS = NO; 412 | CLANG_ANALYZER_NONNULL = YES; 413 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 414 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 415 | CLANG_CXX_LIBRARY = "libc++"; 416 | CLANG_ENABLE_MODULES = YES; 417 | CLANG_ENABLE_OBJC_ARC = YES; 418 | CLANG_ENABLE_OBJC_WEAK = YES; 419 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 420 | CLANG_WARN_BOOL_CONVERSION = YES; 421 | CLANG_WARN_COMMA = YES; 422 | CLANG_WARN_CONSTANT_CONVERSION = YES; 423 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 424 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 425 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 426 | CLANG_WARN_EMPTY_BODY = YES; 427 | CLANG_WARN_ENUM_CONVERSION = YES; 428 | CLANG_WARN_INFINITE_RECURSION = YES; 429 | CLANG_WARN_INT_CONVERSION = YES; 430 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 431 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 432 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 433 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 434 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 435 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 436 | CLANG_WARN_STRICT_PROTOTYPES = YES; 437 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 438 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 439 | CLANG_WARN_UNREACHABLE_CODE = YES; 440 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 441 | COPY_PHASE_STRIP = NO; 442 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 443 | ENABLE_NS_ASSERTIONS = NO; 444 | ENABLE_STRICT_OBJC_MSGSEND = YES; 445 | GCC_C_LANGUAGE_STANDARD = gnu11; 446 | GCC_NO_COMMON_BLOCKS = YES; 447 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 448 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 449 | GCC_WARN_UNDECLARED_SELECTOR = YES; 450 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 451 | GCC_WARN_UNUSED_FUNCTION = YES; 452 | GCC_WARN_UNUSED_VARIABLE = YES; 453 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 454 | MTL_ENABLE_DEBUG_INFO = NO; 455 | MTL_FAST_MATH = YES; 456 | SDKROOT = iphoneos; 457 | SWIFT_COMPILATION_MODE = wholemodule; 458 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 459 | VALIDATE_PRODUCT = YES; 460 | }; 461 | name = Release; 462 | }; 463 | 321AB175255049EA00CE3EFC /* Debug */ = { 464 | isa = XCBuildConfiguration; 465 | buildSettings = { 466 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 467 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 468 | CODE_SIGN_STYLE = Automatic; 469 | DEVELOPMENT_ASSET_PATHS = "\"MultiDatePickerApp/Preview Content\""; 470 | DEVELOPMENT_TEAM = DF5H9M9376; 471 | ENABLE_PREVIEWS = YES; 472 | INFOPLIST_FILE = MultiDatePickerApp/Info.plist; 473 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 474 | LD_RUNPATH_SEARCH_PATHS = ( 475 | "$(inherited)", 476 | "@executable_path/Frameworks", 477 | ); 478 | PRODUCT_BUNDLE_IDENTIFIER = com.keaura.MultiDatePickerApp; 479 | PRODUCT_NAME = "$(TARGET_NAME)"; 480 | SWIFT_VERSION = 5.0; 481 | TARGETED_DEVICE_FAMILY = "1,2"; 482 | }; 483 | name = Debug; 484 | }; 485 | 321AB176255049EA00CE3EFC /* Release */ = { 486 | isa = XCBuildConfiguration; 487 | buildSettings = { 488 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 489 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 490 | CODE_SIGN_STYLE = Automatic; 491 | DEVELOPMENT_ASSET_PATHS = "\"MultiDatePickerApp/Preview Content\""; 492 | DEVELOPMENT_TEAM = DF5H9M9376; 493 | ENABLE_PREVIEWS = YES; 494 | INFOPLIST_FILE = MultiDatePickerApp/Info.plist; 495 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 496 | LD_RUNPATH_SEARCH_PATHS = ( 497 | "$(inherited)", 498 | "@executable_path/Frameworks", 499 | ); 500 | PRODUCT_BUNDLE_IDENTIFIER = com.keaura.MultiDatePickerApp; 501 | PRODUCT_NAME = "$(TARGET_NAME)"; 502 | SWIFT_VERSION = 5.0; 503 | TARGETED_DEVICE_FAMILY = "1,2"; 504 | }; 505 | name = Release; 506 | }; 507 | 321AB178255049EA00CE3EFC /* Debug */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 511 | BUNDLE_LOADER = "$(TEST_HOST)"; 512 | CODE_SIGN_STYLE = Automatic; 513 | DEVELOPMENT_TEAM = DF5H9M9376; 514 | INFOPLIST_FILE = MultiDatePickerAppTests/Info.plist; 515 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 516 | LD_RUNPATH_SEARCH_PATHS = ( 517 | "$(inherited)", 518 | "@executable_path/Frameworks", 519 | "@loader_path/Frameworks", 520 | ); 521 | PRODUCT_BUNDLE_IDENTIFIER = com.keaura.MultiDatePickerAppTests; 522 | PRODUCT_NAME = "$(TARGET_NAME)"; 523 | SWIFT_VERSION = 5.0; 524 | TARGETED_DEVICE_FAMILY = "1,2"; 525 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MultiDatePickerApp.app/MultiDatePickerApp"; 526 | }; 527 | name = Debug; 528 | }; 529 | 321AB179255049EA00CE3EFC /* Release */ = { 530 | isa = XCBuildConfiguration; 531 | buildSettings = { 532 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 533 | BUNDLE_LOADER = "$(TEST_HOST)"; 534 | CODE_SIGN_STYLE = Automatic; 535 | DEVELOPMENT_TEAM = DF5H9M9376; 536 | INFOPLIST_FILE = MultiDatePickerAppTests/Info.plist; 537 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 538 | LD_RUNPATH_SEARCH_PATHS = ( 539 | "$(inherited)", 540 | "@executable_path/Frameworks", 541 | "@loader_path/Frameworks", 542 | ); 543 | PRODUCT_BUNDLE_IDENTIFIER = com.keaura.MultiDatePickerAppTests; 544 | PRODUCT_NAME = "$(TARGET_NAME)"; 545 | SWIFT_VERSION = 5.0; 546 | TARGETED_DEVICE_FAMILY = "1,2"; 547 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MultiDatePickerApp.app/MultiDatePickerApp"; 548 | }; 549 | name = Release; 550 | }; 551 | 321AB17B255049EA00CE3EFC /* Debug */ = { 552 | isa = XCBuildConfiguration; 553 | buildSettings = { 554 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 555 | CODE_SIGN_STYLE = Automatic; 556 | DEVELOPMENT_TEAM = DF5H9M9376; 557 | INFOPLIST_FILE = MultiDatePickerAppUITests/Info.plist; 558 | LD_RUNPATH_SEARCH_PATHS = ( 559 | "$(inherited)", 560 | "@executable_path/Frameworks", 561 | "@loader_path/Frameworks", 562 | ); 563 | PRODUCT_BUNDLE_IDENTIFIER = com.keaura.MultiDatePickerAppUITests; 564 | PRODUCT_NAME = "$(TARGET_NAME)"; 565 | SWIFT_VERSION = 5.0; 566 | TARGETED_DEVICE_FAMILY = "1,2"; 567 | TEST_TARGET_NAME = MultiDatePickerApp; 568 | }; 569 | name = Debug; 570 | }; 571 | 321AB17C255049EA00CE3EFC /* Release */ = { 572 | isa = XCBuildConfiguration; 573 | buildSettings = { 574 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 575 | CODE_SIGN_STYLE = Automatic; 576 | DEVELOPMENT_TEAM = DF5H9M9376; 577 | INFOPLIST_FILE = MultiDatePickerAppUITests/Info.plist; 578 | LD_RUNPATH_SEARCH_PATHS = ( 579 | "$(inherited)", 580 | "@executable_path/Frameworks", 581 | "@loader_path/Frameworks", 582 | ); 583 | PRODUCT_BUNDLE_IDENTIFIER = com.keaura.MultiDatePickerAppUITests; 584 | PRODUCT_NAME = "$(TARGET_NAME)"; 585 | SWIFT_VERSION = 5.0; 586 | TARGETED_DEVICE_FAMILY = "1,2"; 587 | TEST_TARGET_NAME = MultiDatePickerApp; 588 | }; 589 | name = Release; 590 | }; 591 | /* End XCBuildConfiguration section */ 592 | 593 | /* Begin XCConfigurationList section */ 594 | 321AB14A255049E900CE3EFC /* Build configuration list for PBXProject "MultiDatePickerApp" */ = { 595 | isa = XCConfigurationList; 596 | buildConfigurations = ( 597 | 321AB172255049EA00CE3EFC /* Debug */, 598 | 321AB173255049EA00CE3EFC /* Release */, 599 | ); 600 | defaultConfigurationIsVisible = 0; 601 | defaultConfigurationName = Release; 602 | }; 603 | 321AB174255049EA00CE3EFC /* Build configuration list for PBXNativeTarget "MultiDatePickerApp" */ = { 604 | isa = XCConfigurationList; 605 | buildConfigurations = ( 606 | 321AB175255049EA00CE3EFC /* Debug */, 607 | 321AB176255049EA00CE3EFC /* Release */, 608 | ); 609 | defaultConfigurationIsVisible = 0; 610 | defaultConfigurationName = Release; 611 | }; 612 | 321AB177255049EA00CE3EFC /* Build configuration list for PBXNativeTarget "MultiDatePickerAppTests" */ = { 613 | isa = XCConfigurationList; 614 | buildConfigurations = ( 615 | 321AB178255049EA00CE3EFC /* Debug */, 616 | 321AB179255049EA00CE3EFC /* Release */, 617 | ); 618 | defaultConfigurationIsVisible = 0; 619 | defaultConfigurationName = Release; 620 | }; 621 | 321AB17A255049EA00CE3EFC /* Build configuration list for PBXNativeTarget "MultiDatePickerAppUITests" */ = { 622 | isa = XCConfigurationList; 623 | buildConfigurations = ( 624 | 321AB17B255049EA00CE3EFC /* Debug */, 625 | 321AB17C255049EA00CE3EFC /* Release */, 626 | ); 627 | defaultConfigurationIsVisible = 0; 628 | defaultConfigurationName = Release; 629 | }; 630 | /* End XCConfigurationList section */ 631 | }; 632 | rootObject = 321AB147255049E900CE3EFC /* Project object */; 633 | } 634 | -------------------------------------------------------------------------------- /MultiDatePickerApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MultiDatePickerApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MultiDatePickerApp/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 | -------------------------------------------------------------------------------- /MultiDatePickerApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /MultiDatePickerApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MultiDatePickerApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // MultiDatePickerApp 4 | // 5 | // Created by Peter Ent on 11/2/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * This ContentView shows how to use MultiDatePicker as an in-line View. You could also use it as an overlay 12 | * or in a sheet. 13 | */ 14 | struct ContentView: View { 15 | 16 | // The MultiDatePicker can be used to select a single day, multiple days, or a date range. These 17 | // vars are used as bindings passed to the MultiDatePicker. 18 | @State private var selectedDate = Date() 19 | @State private var anyDays = [Date]() 20 | @State private var dateRange: ClosedRange? = nil 21 | 22 | // Another thing you can do with the MultiDatePicker is limit the range of dates that can 23 | // be selected. 24 | let testMinDate = Calendar.current.date(from: DateComponents(year: 2021, month: 4, day: 1)) 25 | let testMaxDate = Calendar.current.date(from: DateComponents(year: 2021, month: 5, day: 30)) 26 | 27 | // Used to toggle an overlay containing a MultiDatePicker. 28 | @State private var showOverlay = false 29 | 30 | var selectedDateAsString: String { 31 | let formatter = DateFormatter() 32 | formatter.dateStyle = .medium 33 | return formatter.string(from: selectedDate) 34 | } 35 | 36 | var body: some View { 37 | TabView { 38 | 39 | // Here's how to select a single date, but limiting the range of dates than be picked. 40 | // The binding (selectedDate) will be whatever date is currently picked. 41 | VStack { 42 | Text("Single Day").font(.title).padding() 43 | MultiDatePicker( 44 | singleDay: self.$selectedDate, 45 | minDate: testMinDate, 46 | maxDate: testMaxDate) 47 | Text(selectedDateAsString).padding() 48 | } 49 | .tabItem { 50 | Image(systemName: "1.circle") 51 | Text("Single") 52 | } 53 | 54 | // Here's how to select multiple, non-contiguous dates. Tapping on a date will 55 | // toggle its selection. The binding (anyDays) will be an array of zero or 56 | // more dates, sorted in ascending order. In this example, the selectable dates 57 | // are limited to weekdays. 58 | VStack { 59 | Text("Any Dates").font(.title).padding() 60 | MultiDatePicker(anyDays: self.$anyDays, includeDays: .weekdaysOnly) 61 | Text("Selected \(anyDays.count) Days").padding() 62 | } 63 | .tabItem { 64 | Image(systemName: "n.circle") 65 | Text("Any") 66 | } 67 | 68 | // Here's how to select a date range. Initially the range is nil. Tapping on 69 | // a date makes it the first date in the range, but the binding (dateRange) is 70 | // still nil. Tapping on another date completes the range and sets the binding. 71 | VStack { 72 | Text("Date Range").font(.title).padding() 73 | MultiDatePicker(dateRange: self.$dateRange) 74 | if let range = dateRange { 75 | Text("\(range)").padding() 76 | } else { 77 | Text("Select two dates").padding() 78 | } 79 | } 80 | .tabItem { 81 | Image(systemName: "ellipsis.circle") 82 | Text("Range") 83 | } 84 | 85 | // Here's how to put the MultiDatePicker into a pop-over/dialog using 86 | // the .overlay modifier (see below). 87 | VStack { 88 | Text("Pop-Over").font(.title).padding() 89 | Button("Selected \(anyDays.count) Days") { 90 | withAnimation { 91 | self.showOverlay.toggle() 92 | } 93 | }.padding() 94 | } 95 | .tabItem { 96 | Image(systemName: "square.stack.3d.up") 97 | Text("Overlay") 98 | } 99 | } 100 | .onChange(of: self.selectedDate, perform: { date in 101 | print("Single date selected: \(date)") 102 | }) 103 | .onChange(of: self.anyDays, perform: { days in 104 | print("Any days selected: \(days)") 105 | }) 106 | .onChange(of: self.dateRange, perform: { dateRange in 107 | print("Range selected: \(String(describing: dateRange))") 108 | }) 109 | 110 | // if you want to show the MultiDatePicker as an overlay somewhat similar to the Apple 111 | // DatePicker, you can blur the background a bit and float the MultiDatePicker above a 112 | // translucent background. Tapping outside of the MultiDatePicker removes it. Ideally 113 | // you'd make this a custom modifier if you were doing this throughout your app. 114 | .blur(radius: showOverlay ? 6 : 0) 115 | .overlay( 116 | ZStack { 117 | if self.showOverlay { 118 | Color.black.opacity(0.25) 119 | .edgesIgnoringSafeArea(.all) 120 | .onTapGesture { 121 | withAnimation { 122 | self.showOverlay.toggle() 123 | } 124 | } 125 | MultiDatePicker(anyDays: self.$anyDays) 126 | } else { 127 | EmptyView() 128 | } 129 | } 130 | ) 131 | } 132 | } 133 | 134 | struct ContentView_Previews: PreviewProvider { 135 | static var previews: some View { 136 | ContentView() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /MultiDatePickerApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /MultiDatePickerApp/MultiDatePicker Source/Components/MDPContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MonthContentView.swift 3 | // MultiDatePickerApp 4 | // 5 | // Created by Peter Ent on 11/3/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * Displays the calendar of MDPDayOfMonth items using MDPDayView views. 12 | */ 13 | struct MDPContentView: View { 14 | @EnvironmentObject var monthDataModel: MDPModel 15 | 16 | let cellSize: CGFloat = 30 17 | 18 | let columns = [ 19 | GridItem(.fixed(30), spacing: 2), 20 | GridItem(.fixed(30), spacing: 2), 21 | GridItem(.fixed(30), spacing: 2), 22 | GridItem(.fixed(30), spacing: 2), 23 | GridItem(.fixed(30), spacing: 2), 24 | GridItem(.fixed(30), spacing: 2), 25 | GridItem(.fixed(30), spacing: 2) 26 | ] 27 | 28 | var body: some View { 29 | LazyVGrid(columns: columns, spacing: 0) { 30 | 31 | // Sun, Mon, etc. 32 | ForEach(0..? 52 | private var anyDatesWrapper: Binding<[Date]>? 53 | private var dateRangeWrapper: Binding?>? 54 | 55 | private var minDate: Date? = nil 56 | private var maxDate: Date? = nil 57 | 58 | // the type of date picker 59 | private var pickerType: MultiDatePicker.PickerType = .singleDay 60 | 61 | // which days are available for selection 62 | private var selectionType: MultiDatePicker.DateSelectionChoices = .allDays 63 | 64 | // the actual number of days in this calendar month/year (eg, 28 for February) 65 | private var numDays = 0 66 | 67 | // MARK: - INIT 68 | 69 | convenience init(anyDays: Binding<[Date]>, 70 | includeDays: MultiDatePicker.DateSelectionChoices, 71 | minDate: Date?, 72 | maxDate: Date?) { 73 | self.init() 74 | self.anyDatesWrapper = anyDays 75 | self.selectionType = includeDays 76 | self.minDate = minDate 77 | self.maxDate = maxDate 78 | setSelection(anyDays.wrappedValue) 79 | 80 | // set the controlDate to be the first of the anyDays if the 81 | // anyDays array is not empty. 82 | if let useDate = anyDays.wrappedValue.first { 83 | controlDate = useDate 84 | } 85 | buildDays() 86 | } 87 | 88 | convenience init(singleDay: Binding, 89 | includeDays: MultiDatePicker.DateSelectionChoices, 90 | minDate: Date?, 91 | maxDate: Date?) { 92 | self.init() 93 | self.singleDayWrapper = singleDay 94 | self.selectionType = includeDays 95 | self.minDate = minDate 96 | self.maxDate = maxDate 97 | setSelection(singleDay.wrappedValue) 98 | 99 | // set the controlDate to be this singleDay 100 | controlDate = singleDay.wrappedValue 101 | buildDays() 102 | } 103 | 104 | convenience init(dateRange: Binding?>, 105 | includeDays: MultiDatePicker.DateSelectionChoices, 106 | minDate: Date?, 107 | maxDate: Date?) { 108 | self.init() 109 | self.dateRangeWrapper = dateRange 110 | self.selectionType = includeDays 111 | self.minDate = minDate 112 | self.maxDate = maxDate 113 | setSelection(dateRange.wrappedValue) 114 | 115 | // set the selection to be the first in the range if the range exists 116 | if let dateRange = dateRange.wrappedValue { 117 | controlDate = dateRange.lowerBound 118 | } 119 | buildDays() 120 | } 121 | 122 | // MARK: - PUBLIC 123 | 124 | func dayOfMonth(byDay: Int) -> MDPDayOfMonth? { 125 | guard 1 <= byDay && byDay <= 31 else { return nil } 126 | for dom in days { 127 | if dom.day == byDay { 128 | return dom 129 | } 130 | } 131 | return nil 132 | } 133 | 134 | func selectDay(_ day: MDPDayOfMonth) { 135 | guard day.isSelectable else { return } 136 | guard let date = day.date else { return } 137 | 138 | switch pickerType { 139 | 140 | // just make the date the new selection 141 | case .singleDay: 142 | selections = [date] 143 | singleDayWrapper?.wrappedValue = date 144 | 145 | // just add the date to the selection list, don't forget to sort it 146 | case .anyDays: 147 | if selections.contains(date), let pos = selections.firstIndex(of: date) { 148 | selections.remove(at: pos) 149 | } else { 150 | selections.append(date) 151 | } 152 | selections.sort() 153 | anyDatesWrapper?.wrappedValue = selections 154 | 155 | // if the selection has 1 element, it completes the range else it starts 156 | // a new range 157 | default: 158 | if selections.count != 1 { 159 | selections = [date] 160 | } else { 161 | selections.append(date) 162 | } 163 | selections.sort() 164 | if selections.count == 2 { 165 | dateRangeWrapper?.wrappedValue = selections[0]...selections[1] 166 | } else { 167 | dateRangeWrapper?.wrappedValue = nil 168 | } 169 | } 170 | } 171 | 172 | func isSelected(_ day: MDPDayOfMonth) -> Bool { 173 | guard day.isSelectable else { return false } 174 | guard let date = day.date else { return false } 175 | 176 | if pickerType == .anyDays || pickerType == .singleDay { 177 | for test in selections { 178 | if isSameDay(date1: test, date2: date) { 179 | return true 180 | } 181 | } 182 | } else { 183 | if selections.count == 0 { 184 | return false 185 | } 186 | else if selections.count == 1 { 187 | return isSameDay(date1: selections[0], date2: date) 188 | } else { 189 | let range = selections[0]...selections[1] 190 | return range.contains(date) 191 | } 192 | } 193 | return false 194 | } 195 | 196 | func incrMonth() { 197 | let calendar = Calendar.current 198 | if let newDate = calendar.date(byAdding: .month, value: 1, to: controlDate) { 199 | controlDate = newDate 200 | } 201 | } 202 | 203 | func decrMonth() { 204 | let calendar = Calendar.current 205 | if let newDate = calendar.date(byAdding: .month, value: -1, to: controlDate) { 206 | controlDate = newDate 207 | } 208 | } 209 | 210 | func show(month: Int, year: Int) { 211 | let calendar = Calendar.current 212 | let components = DateComponents(year: year, month: month, day: 1) 213 | if let newDate = calendar.date(from: components) { 214 | controlDate = newDate 215 | } 216 | } 217 | 218 | } 219 | 220 | // MARK: - BUILD DAYS 221 | 222 | extension MDPModel { 223 | 224 | private func buildDays() { 225 | let calendar = Calendar.current 226 | let year = calendar.component(.year, from: controlDate) 227 | let month = calendar.component(.month, from: controlDate) 228 | 229 | let dateComponents = DateComponents(year: year, month: month) 230 | let date = calendar.date(from: dateComponents)! 231 | 232 | let range = calendar.range(of: .day, in: .month, for: date)! 233 | let numDays = range.count 234 | 235 | let ord = calendar.component(.weekday, from: date) 236 | var index = 0 237 | 238 | let today = Date() 239 | 240 | // create an empty int array 241 | var daysArray = [MDPDayOfMonth]() 242 | 243 | // for 0 to ord, set the value in the array[index] to be 0, meaning no day here. 244 | for _ in 1..?) { 294 | pickerType = .dateRange 295 | if let dateRange = dateRange { 296 | selections = [dateRange.lowerBound, dateRange.upperBound] 297 | } 298 | } 299 | 300 | private func isSameDay(date1: Date?, date2: Date?) -> Bool { 301 | guard let date1 = date1, let date2 = date2 else { return false } 302 | let day1 = Calendar.current.component(.day, from: date1) 303 | let day2 = Calendar.current.component(.day, from: date2) 304 | let year1 = Calendar.current.component(.year, from: date1) 305 | let year2 = Calendar.current.component(.year, from: date2) 306 | let month1 = Calendar.current.component(.month, from: date1) 307 | let month2 = Calendar.current.component(.month, from: date2) 308 | return (day1 == day2) && (month1 == month2) && (year1 == year2) 309 | } 310 | 311 | private func isEligible(date: Date?) -> Bool { 312 | guard let date = date else { return true } 313 | 314 | if let minDate = minDate, let maxDate = maxDate { 315 | return (minDate...maxDate).contains(date) 316 | } else if let minDate = minDate { 317 | return date >= minDate 318 | } else if let maxDate = maxDate { 319 | return date <= maxDate 320 | } 321 | 322 | switch selectionType { 323 | case .weekendsOnly: 324 | let ord = Calendar.current.component(.weekday, from: date) 325 | return ord == 1 || ord == 7 326 | case .weekdaysOnly: 327 | let ord = Calendar.current.component(.weekday, from: date) 328 | return 1 < ord && ord < 7 329 | default: 330 | return true 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /MultiDatePickerApp/MultiDatePicker Source/Components/MDPMonthView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MonthView.swift 3 | // MultiDatePickerApp 4 | // 5 | // Created by Peter Ent on 11/2/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * MDPMonthView is really the crux of the control. This displays everything and handles the interactions 12 | * and selections. MulitDatePicker is the public interface that sets up the model and this view. 13 | */ 14 | struct MDPMonthView: View { 15 | @EnvironmentObject var monthDataModel: MDPModel 16 | 17 | @State private var showMonthYearPicker = false 18 | @State private var testDate = Date() 19 | 20 | private func showPrevMonth() { 21 | withAnimation { 22 | monthDataModel.decrMonth() 23 | showMonthYearPicker = false 24 | } 25 | } 26 | 27 | private func showNextMonth() { 28 | withAnimation { 29 | monthDataModel.incrMonth() 30 | showMonthYearPicker = false 31 | } 32 | } 33 | 34 | var body: some View { 35 | VStack { 36 | HStack { 37 | MDPMonthYearPickerButton(isPresented: self.$showMonthYearPicker) 38 | Spacer() 39 | Button( action: {showPrevMonth()} ) { 40 | Image(systemName: "chevron.left").font(.title2) 41 | }.padding() 42 | Button( action: {showNextMonth()} ) { 43 | Image(systemName: "chevron.right").font(.title2) 44 | }.padding() 45 | } 46 | .padding(.leading, 18) 47 | 48 | GeometryReader { reader in 49 | if showMonthYearPicker { 50 | MDPMonthYearPicker(date: monthDataModel.controlDate) { (month, year) in 51 | self.monthDataModel.show(month: month, year: year) 52 | } 53 | } 54 | else { 55 | MDPContentView() 56 | } 57 | } 58 | } 59 | .background( 60 | RoundedRectangle(cornerRadius: 10) 61 | .foregroundColor(.white) 62 | ) 63 | .overlay( 64 | RoundedRectangle(cornerRadius: 10) 65 | .stroke(Color.accentColor, lineWidth: 1) 66 | ) 67 | .padding() 68 | .frame(width: 300, height: 300) 69 | } 70 | } 71 | 72 | struct MonthView_Previews: PreviewProvider { 73 | static var previews: some View { 74 | MDPMonthView() 75 | .environmentObject(MDPModel()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /MultiDatePickerApp/MultiDatePicker Source/Components/MDPMonthYearPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MonthYearPicker.swift 3 | // MultiDatePickerApp 4 | // 5 | // Created by Peter Ent on 11/2/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * This is a two-wheel picker for selecting a month and a year. It appears when the user 12 | * taps on the month/year at the top of the MDMonthView. 13 | * 14 | * When a month or year is selected, the action parameter is invoked with the new values. 15 | */ 16 | struct MDPMonthYearPicker: View { 17 | let months = (0...11).map {$0} 18 | let years = (1970...2099).map {$0} 19 | 20 | var date: Date 21 | var action: (Int, Int) -> Void 22 | 23 | @State private var selectedMonth = 0 24 | @State private var selectedYear = 2020 25 | 26 | init(date: Date, action: @escaping (Int, Int) -> Void) { 27 | self.date = date 28 | self.action = action 29 | 30 | let calendar = Calendar.current 31 | let month = calendar.component(.month, from: date) 32 | let year = calendar.component(.year, from: date) 33 | 34 | self._selectedMonth = State(initialValue: month - 1) 35 | self._selectedYear = State(initialValue: year) 36 | } 37 | 38 | var body: some View { 39 | HStack(alignment: .center, spacing: 0) { 40 | Picker("", selection: self.$selectedMonth) { 41 | ForEach(months, id: \.self) { month in 42 | Text("\(Calendar.current.monthSymbols[month])").tag(month) 43 | } 44 | } 45 | .onChange(of: selectedMonth, perform: { value in 46 | self.action(value + 1, self.selectedYear) 47 | }) 48 | .frame(width: 150) 49 | .clipped() 50 | 51 | Picker("", selection: self.$selectedYear) { 52 | ForEach(years, id: \.self) { year in 53 | Text(String(format: "%d", year)).tag(year) 54 | } 55 | } 56 | .onChange(of: selectedYear, perform: { value in 57 | self.action(self.selectedMonth + 1, value) 58 | }) 59 | .frame(width: 100) 60 | .clipped() 61 | } 62 | } 63 | } 64 | 65 | struct MonthYearPicker_Previews: PreviewProvider { 66 | static var previews: some View { 67 | MDPMonthYearPicker(date: Date()) { (month, year) in 68 | print("You picked \(month), \(year)") 69 | } 70 | .frame(width: 300, height: 300) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MultiDatePickerApp/MultiDatePicker Source/Components/MDPMonthYearPickerButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MonthYearPickerButton.swift 3 | // MultiDatePickerApp 4 | // 5 | // Created by Peter Ent on 11/3/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * The MDPMonthYearPickerButton sits at the top of the MDPMonthView and displays the current month 12 | * and year showing in the view. Tapping this control switches the main view to the month/year 13 | * picker. 14 | * 15 | * This is a quick way for the user to jump the year or month without having to the < or > 16 | * buttons. 17 | */ 18 | struct MDPMonthYearPickerButton: View { 19 | @EnvironmentObject var monthDataModel: MDPModel 20 | 21 | @Binding var isPresented: Bool 22 | 23 | var body: some View { 24 | Button( action: {withAnimation { isPresented.toggle()} } ) { 25 | HStack { 26 | Text(monthDataModel.title) 27 | .font(.subheadline) 28 | .fontWeight(.semibold) 29 | .foregroundColor(self.isPresented ? .accentColor : .black) 30 | Image(systemName: "chevron.right") 31 | .rotationEffect(self.isPresented ? .degrees(90) : .degrees(0)) 32 | } 33 | } 34 | } 35 | } 36 | 37 | struct MonthYearPickerButton_Previews: PreviewProvider { 38 | static var previews: some View { 39 | MDPMonthYearPickerButton(isPresented: .constant(false)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MultiDatePickerApp/MultiDatePicker Source/MultiDatePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiDatePicker.swift 3 | // MultiDatePickerApp 4 | // 5 | // Created by Peter Ent on 11/3/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * This component shows a date picker very similar to Apple's SwiftUI 2.0 DatePicker, but with a difference. 12 | * Instead of just allowing a single date to be picked, the MultiDatePicker also allows the user to select 13 | * a set of non-contiguous dates or a date range. It just depends on how this View is initialized. 14 | * 15 | * init(singleDay: Binding [,options]) 16 | * A single-date picker. Selecting a date de-selects the previous selection. Because the binding 17 | * is a Date, there is always a selected date. 18 | * 19 | * init(anyDates: Binding<[Date]>, [,options]) 20 | * Allows multiple, non-continguous, dates to be selected. De-select a date by tapping it again. 21 | * The binding array may be empty or it will be an array of dates selected in ascending order. 22 | * 23 | * init(dateRange: Binding?>, [,options]) 24 | * Selects a date range. Tapping on a date marks it as the first date, tapping a second date 25 | * completes the range. Tapping a date again resets the range. The binding will be nil unless 26 | * two dates are selected, completeing the range. 27 | * 28 | * optional parameters to init() functions are: 29 | * - includeDays: .allDays, .weekdaysOnly, .weekendsOnly 30 | * Days not selectable are shown in gray and not selected. 31 | * - minDate: Date? = nil 32 | * Days before minDate are not selectable. 33 | * - maxDate: Date? = nil 34 | * Days after maxDate are not selectable. 35 | */ 36 | struct MultiDatePicker: View { 37 | 38 | // the type of picker, based on which init() function is used. 39 | enum PickerType { 40 | case singleDay 41 | case anyDays 42 | case dateRange 43 | } 44 | 45 | // lets all or some dates be elligible for selection. 46 | enum DateSelectionChoices { 47 | case allDays 48 | case weekendsOnly 49 | case weekdaysOnly 50 | } 51 | 52 | @StateObject var monthModel: MDPModel 53 | 54 | // selects only a single date 55 | 56 | init(singleDay: Binding, 57 | includeDays: DateSelectionChoices = .allDays, 58 | minDate: Date? = nil, 59 | maxDate: Date? = nil 60 | ) { 61 | _monthModel = StateObject(wrappedValue: MDPModel(singleDay: singleDay, includeDays: includeDays, minDate: minDate, maxDate: maxDate)) 62 | } 63 | 64 | // selects any number of dates, non-contiguous 65 | 66 | init(anyDays: Binding<[Date]>, 67 | includeDays: DateSelectionChoices = .allDays, 68 | minDate: Date? = nil, 69 | maxDate: Date? = nil 70 | ) { 71 | _monthModel = StateObject(wrappedValue: MDPModel(anyDays: anyDays, includeDays: includeDays, minDate: minDate, maxDate: maxDate)) 72 | } 73 | 74 | // selects a closed date range 75 | 76 | init(dateRange: Binding?>, 77 | includeDays: DateSelectionChoices = .allDays, 78 | minDate: Date? = nil, 79 | maxDate: Date? = nil 80 | ) { 81 | _monthModel = StateObject(wrappedValue: MDPModel(dateRange: dateRange, includeDays: includeDays, minDate: minDate, maxDate: maxDate)) 82 | } 83 | 84 | var body: some View { 85 | MDPMonthView() 86 | .environmentObject(monthModel) 87 | } 88 | } 89 | 90 | struct MultiDatePicker_Previews: PreviewProvider { 91 | @State static var oneDay = Date() 92 | @State static var manyDates = [Date]() 93 | @State static var dateRange: ClosedRange? = nil 94 | 95 | static var previews: some View { 96 | ScrollView { 97 | VStack { 98 | MultiDatePicker(singleDay: $oneDay, includeDays: .weekdaysOnly) 99 | MultiDatePicker(anyDays: $manyDates, includeDays: .weekendsOnly) 100 | MultiDatePicker(dateRange: $dateRange) 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /MultiDatePickerApp/MultiDatePickerAppApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiDatePickerAppApp.swift 3 | // MultiDatePickerApp 4 | // 5 | // Created by Peter Ent on 11/2/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct MultiDatePickerAppApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MultiDatePickerApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MultiDatePickerAppTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MultiDatePickerAppTests/MultiDatePickerAppTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiDatePickerAppTests.swift 3 | // MultiDatePickerAppTests 4 | // 5 | // Created by Peter Ent on 11/2/20. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | @testable import MultiDatePickerApp 11 | 12 | class MultiDatePickerAppTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testMonthDataModel() throws { 23 | let model = MDPModel() 24 | let dateComponents = DateComponents(year: 2020, month: 11, day: 1) 25 | model.controlDate = Calendar.current.date(from: dateComponents)! 26 | XCTAssertTrue(model.numDays == 30) 27 | } 28 | 29 | func testSingleDaySelection() throws { 30 | let dateComponents = DateComponents(year: 2020, month: 11, day: 1) 31 | let singleDay = Calendar.current.date(from: dateComponents) 32 | XCTAssertNotNil(singleDay) 33 | 34 | let model = MDPModel(singleDay: .constant(singleDay!), includeDays: .allDays, minDate: nil, maxDate: nil) 35 | model.controlDate = singleDay! 36 | 37 | let dom = MDPDayOfMonth(index: 0, day: 1, date: singleDay, isSelectable: true, isToday: false) 38 | model.selectDay(dom) 39 | let isSelected = model.isSelected(dom) 40 | XCTAssertTrue(isSelected) 41 | } 42 | 43 | func testSelectionInRange() throws { 44 | let dateComponents = DateComponents(year: 2020, month: 10, day: 15) 45 | let singleDay = Calendar.current.date(from: dateComponents) 46 | XCTAssertNotNil(singleDay) 47 | 48 | let minDate = Calendar.current.date(from: DateComponents(year: 2020, month: 10, day: 1)) 49 | XCTAssertNotNil(minDate) 50 | 51 | let maxDate = Calendar.current.date(from: DateComponents(year: 2020, month: 10, day: 30)) 52 | XCTAssertNotNil(maxDate) 53 | 54 | let model = MDPModel(singleDay: .constant(singleDay!), includeDays: .allDays, minDate: minDate!, maxDate: maxDate!) 55 | model.controlDate = singleDay! 56 | 57 | // date should be between the min and max dates 58 | 59 | let dom = model.dayOfMonth(byDay: 15) 60 | XCTAssertNotNil(dom) 61 | XCTAssertTrue(dom!.isSelectable) 62 | } 63 | 64 | func testSelectionNotInRange() throws { 65 | let dateComponents = DateComponents(year: 2020, month: 10, day: 15) 66 | let singleDay = Calendar.current.date(from: dateComponents) 67 | XCTAssertNotNil(singleDay) 68 | 69 | let minDate = Calendar.current.date(from: DateComponents(year: 2020, month: 10, day: 10)) 70 | XCTAssertNotNil(minDate) 71 | 72 | let maxDate = Calendar.current.date(from: DateComponents(year: 2020, month: 10, day: 20)) 73 | XCTAssertNotNil(maxDate) 74 | 75 | let model = MDPModel(singleDay: .constant(singleDay!), includeDays: .allDays, minDate: minDate!, maxDate: maxDate!) 76 | model.controlDate = singleDay! 77 | 78 | // date should outside the min and max date range 79 | 80 | let dom = model.dayOfMonth(byDay: 1) 81 | XCTAssertNotNil(dom) 82 | XCTAssertFalse(dom!.isSelectable) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /MultiDatePickerAppUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MultiDatePickerAppUITests/MultiDatePickerAppUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiDatePickerAppUITests.swift 3 | // MultiDatePickerAppUITests 4 | // 5 | // Created by Peter Ent on 11/2/20. 6 | // 7 | 8 | import XCTest 9 | 10 | class MultiDatePickerAppUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | // func testExample() throws { 26 | // // UI tests must launch the application that they test. 27 | // let app = XCUIApplication() 28 | // app.launch() 29 | // 30 | // // Use recording to get started writing UI tests. 31 | // // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | // } 33 | 34 | // func testLaunchPerformance() throws { 35 | // if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // // This measures how long it takes to launch your application. 37 | // measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | // XCUIApplication().launch() 39 | // } 40 | // } 41 | // } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiDatePicker - A SwiftUI Component 2 | 3 | > Requires iOS 14, Xcode 12 4 | 5 | Many applications have the need to pick a date. But what if you have an app that requires more than one date to be selected by the user? Perhaps you are writing a hotel reservation system or an employee scheduler and need the user to select starting and ending dates or just all the days they will be working this month. 6 | 7 | This is where `MultiDataPicker` comes in. The standard Apple `DatePicker` lets you pick a single day. `MultiDatePicker` lets you pick a single day, a collection of days, or a date range. Its all in how you set up the component. 8 | 9 | The `ContentView` gives you examples of each type of selection mode `MultiDatePicker` is capable of. You need a variable to use for binding and then just use it: 10 | 11 | > I've updated the code to let the selection determine the calendar presented. Before this update, the selections would appear, but the calendar would always show the month/year for the current date. With this update, the calendar reflects the selection. 12 | 13 | ### Single Day 14 | 15 | ``` 16 | @State var selectedDate = Date() 17 | MultiDatePicker(singleDay: self.$selectedDate) 18 | ``` 19 | Whenever the user taps a date on the control's calendar the wrapped value will change. The calendar shows the month and year for the date given. 20 | 21 | ### Collection of Days 22 | 23 | ``` 24 | @State var manyDays = [Date]() 25 | MultiDatePicker(anyDays: self.$manyDays) 26 | ``` 27 | 28 | Whenever the user taps on a date on the control's calendar the date will be selected and added to the wrapped collection. If the date is already selected, the tap will remove it from the collection and change the wrapped value of the binding. 29 | 30 | The calendar in the control will reflect the first date in the array; otherwise it will show the month/year for the current date. 31 | 32 | ### Date Range 33 | 34 | ``` 35 | @State var range: ClosedRange? = nil 36 | MultiDatePicker(dateRange: self.$range) 37 | ``` 38 | The wrapped value of the binding only changes if two dates are selected. If a third date is picked, the wrapped value is reset to `nil` and the range is removed from the calendar leaving only the one date selected. The user has to tap another date to complete the range and change the wrapped value. 39 | 40 | The calendar in the control will show the month/year for the first day of the range. If the range is nil it will show the current month/year. 41 | 42 | ## More Options 43 | 44 | The `MultiDatePicker` has few other options you can pass into it to limit which days can be selected. 45 | 46 | The `includeDays` parameter can be one of `.allDays` (the default), `weekendOnly`, or `weekdaysOnly`. For example if you pass `includeDays=.weekdaysOnly` then all weekend days appear gray and cannot be selected. 47 | 48 | You can also set a `minDate` and/or a `maxDate` (both of which default to `nil`). Dates before `minDate` or after `maxDate` cannot be selected and appear gray. 49 | 50 | ## Notes 51 | 52 | You can find out more on my blog at http://www.keaura.com/blog but basically: 53 | 54 | - Download this repository 55 | - Build the app to test it out 56 | - Copy the MultiDatePicker folder into your app source 57 | 58 | I've put a bunch of comments throughout the code and it should be fairly easy to integrate. 59 | 60 | I hope you find this useful. 61 | --------------------------------------------------------------------------------