├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── JBCalendarDatePicker.xcscheme ├── JBCalendarDatePicker.podspec ├── JBCalendarDatePicker.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── compc.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Info.plist └── JBCalendarDatePicker │ ├── CalendarDatePickerViewController.h │ ├── DateInputView.swift │ ├── Day.swift │ ├── JBCalendarDateCell.swift │ ├── JBCalendarDateCell.xib │ ├── JBCalendarDatePicker.h │ ├── JBCalendarViewController.swift │ ├── JBCalendarViewController.xib │ ├── JBDatePicker.swift │ ├── JBDatePickerViewController.swift │ ├── JBDatePickerViewController.xib │ └── UIColor+SystemAccent.swift └── Tests └── JBCalendarDatePickerTests ├── JBCalendarDatePickerTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/JBCalendarDatePicker.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 81 | 82 | 88 | 89 | 95 | 96 | 97 | 98 | 100 | 101 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /JBCalendarDatePicker.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | # 1 4 | s.platform = :ios 5 | s.ios.deployment_target = '13.0' 6 | s.name = "JBCalendarDatePicker" 7 | s.summary = "A replacement for UIDatePicker made for Catalyst." 8 | s.requires_arc = true 9 | 10 | # 2 11 | s.version = "0.2.3" 12 | 13 | # 3 14 | s.license = { :type => "MIT", :file => "LICENSE" } 15 | 16 | # 4 - Replace with your name and e-mail address 17 | s.author = { "Josh Birnholz" => "josh@birnholz.com" } 18 | 19 | # 5 - Replace this URL with your own GitHub page's URL (from the address bar) 20 | s.homepage = "https://github.com/joshbirnholz/JBCalendarDatePicker" 21 | 22 | # 6 - Replace this URL with your own Git URL from "Quick Setup" 23 | s.source = { :git => "https://github.com/joshbirnholz/JBCalendarDatePicker.git", 24 | :tag => "#{s.version}" } 25 | 26 | # 7 27 | s.framework = "UIKit" 28 | 29 | # 8 30 | s.source_files = "JBCalendarDatePicker/**/*.{swift}" 31 | 32 | # 9 33 | s.resources = "JBCalendarDatePicker/**/*.{xib}" 34 | 35 | # 10 36 | s.swift_version = "5" 37 | 38 | end 39 | -------------------------------------------------------------------------------- /JBCalendarDatePicker.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | "JBCalendarDatePicker::JBCalendarDatePickerPackageTests::ProductTarget" /* JBCalendarDatePickerPackageTests */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = OBJ_47 /* Build configuration list for PBXAggregateTarget "JBCalendarDatePickerPackageTests" */; 13 | buildPhases = ( 14 | ); 15 | dependencies = ( 16 | OBJ_50 /* PBXTargetDependency */, 17 | ); 18 | name = JBCalendarDatePickerPackageTests; 19 | productName = JBCalendarDatePickerPackageTests; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | OBJ_32 /* DateInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* DateInputView.swift */; }; 25 | OBJ_33 /* Day.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* Day.swift */; }; 26 | OBJ_34 /* JBCalendarDateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* JBCalendarDateCell.swift */; }; 27 | OBJ_35 /* JBCalendarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* JBCalendarViewController.swift */; }; 28 | OBJ_36 /* JBDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* JBDatePicker.swift */; }; 29 | OBJ_37 /* JBDatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* JBDatePickerViewController.swift */; }; 30 | OBJ_38 /* UIColor+SystemAccent.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* UIColor+SystemAccent.swift */; }; 31 | OBJ_45 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; 32 | OBJ_56 /* JBCalendarDatePickerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* JBCalendarDatePickerTests.swift */; }; 33 | OBJ_57 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* XCTestManifests.swift */; }; 34 | OBJ_59 /* JBCalendarDatePicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "JBCalendarDatePicker::JBCalendarDatePicker::Product" /* JBCalendarDatePicker.framework */; }; 35 | /* End PBXBuildFile section */ 36 | 37 | /* Begin PBXContainerItemProxy section */ 38 | E4DA685325A9A5A900FE1035 /* PBXContainerItemProxy */ = { 39 | isa = PBXContainerItemProxy; 40 | containerPortal = OBJ_1 /* Project object */; 41 | proxyType = 1; 42 | remoteGlobalIDString = "JBCalendarDatePicker::JBCalendarDatePicker"; 43 | remoteInfo = JBCalendarDatePicker; 44 | }; 45 | /* End PBXContainerItemProxy section */ 46 | 47 | /* Begin PBXFileReference section */ 48 | E4DA685425A9A5C200FE1035 /* JBDatePickerViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = JBDatePickerViewController.xib; sourceTree = ""; }; 49 | E4DA685525A9A5CB00FE1035 /* JBCalendarDateCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = JBCalendarDateCell.xib; sourceTree = ""; }; 50 | E4DA685625A9A5CB00FE1035 /* JBCalendarViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = JBCalendarViewController.xib; sourceTree = ""; }; 51 | "JBCalendarDatePicker::JBCalendarDatePicker::Product" /* JBCalendarDatePicker.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = JBCalendarDatePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | "JBCalendarDatePicker::JBCalendarDatePickerTests::Product" /* JBCalendarDatePickerTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = JBCalendarDatePickerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | OBJ_10 /* DateInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateInputView.swift; sourceTree = ""; }; 54 | OBJ_11 /* Day.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Day.swift; sourceTree = ""; }; 55 | OBJ_12 /* JBCalendarDateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JBCalendarDateCell.swift; sourceTree = ""; }; 56 | OBJ_13 /* JBCalendarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JBCalendarViewController.swift; sourceTree = ""; }; 57 | OBJ_14 /* JBDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JBDatePicker.swift; sourceTree = ""; }; 58 | OBJ_15 /* JBDatePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JBDatePickerViewController.swift; sourceTree = ""; }; 59 | OBJ_16 /* UIColor+SystemAccent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+SystemAccent.swift"; sourceTree = ""; }; 60 | OBJ_19 /* JBCalendarDatePickerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JBCalendarDatePickerTests.swift; sourceTree = ""; }; 61 | OBJ_20 /* XCTestManifests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = ""; }; 62 | OBJ_24 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 63 | OBJ_25 /* JBCalendarDatePicker.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = JBCalendarDatePicker.podspec; sourceTree = ""; }; 64 | OBJ_26 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 65 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 66 | OBJ_8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67 | /* End PBXFileReference section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | OBJ_39 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 0; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | OBJ_58 /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 0; 80 | files = ( 81 | OBJ_59 /* JBCalendarDatePicker.framework in Frameworks */, 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | /* End PBXFrameworksBuildPhase section */ 86 | 87 | /* Begin PBXGroup section */ 88 | OBJ_17 /* Tests */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | OBJ_18 /* JBCalendarDatePickerTests */, 92 | ); 93 | path = Tests; 94 | sourceTree = SOURCE_ROOT; 95 | }; 96 | OBJ_18 /* JBCalendarDatePickerTests */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | OBJ_19 /* JBCalendarDatePickerTests.swift */, 100 | OBJ_20 /* XCTestManifests.swift */, 101 | ); 102 | path = JBCalendarDatePickerTests; 103 | sourceTree = ""; 104 | }; 105 | OBJ_21 /* Products */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | "JBCalendarDatePicker::JBCalendarDatePickerTests::Product" /* JBCalendarDatePickerTests.xctest */, 109 | "JBCalendarDatePicker::JBCalendarDatePicker::Product" /* JBCalendarDatePicker.framework */, 110 | ); 111 | name = Products; 112 | sourceTree = BUILT_PRODUCTS_DIR; 113 | }; 114 | OBJ_5 /* */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | OBJ_6 /* Package.swift */, 118 | OBJ_7 /* Sources */, 119 | OBJ_17 /* Tests */, 120 | OBJ_21 /* Products */, 121 | OBJ_24 /* LICENSE */, 122 | OBJ_25 /* JBCalendarDatePicker.podspec */, 123 | OBJ_26 /* README.md */, 124 | ); 125 | name = ""; 126 | sourceTree = ""; 127 | }; 128 | OBJ_7 /* Sources */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | OBJ_8 /* Info.plist */, 132 | OBJ_9 /* JBCalendarDatePicker */, 133 | ); 134 | path = Sources; 135 | sourceTree = SOURCE_ROOT; 136 | }; 137 | OBJ_9 /* JBCalendarDatePicker */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | E4DA685525A9A5CB00FE1035 /* JBCalendarDateCell.xib */, 141 | E4DA685625A9A5CB00FE1035 /* JBCalendarViewController.xib */, 142 | E4DA685425A9A5C200FE1035 /* JBDatePickerViewController.xib */, 143 | OBJ_10 /* DateInputView.swift */, 144 | OBJ_11 /* Day.swift */, 145 | OBJ_12 /* JBCalendarDateCell.swift */, 146 | OBJ_13 /* JBCalendarViewController.swift */, 147 | OBJ_14 /* JBDatePicker.swift */, 148 | OBJ_15 /* JBDatePickerViewController.swift */, 149 | OBJ_16 /* UIColor+SystemAccent.swift */, 150 | ); 151 | path = JBCalendarDatePicker; 152 | sourceTree = ""; 153 | }; 154 | /* End PBXGroup section */ 155 | 156 | /* Begin PBXNativeTarget section */ 157 | "JBCalendarDatePicker::JBCalendarDatePicker" /* JBCalendarDatePicker */ = { 158 | isa = PBXNativeTarget; 159 | buildConfigurationList = OBJ_28 /* Build configuration list for PBXNativeTarget "JBCalendarDatePicker" */; 160 | buildPhases = ( 161 | OBJ_31 /* Sources */, 162 | OBJ_39 /* Frameworks */, 163 | ); 164 | buildRules = ( 165 | ); 166 | dependencies = ( 167 | ); 168 | name = JBCalendarDatePicker; 169 | productName = JBCalendarDatePicker; 170 | productReference = "JBCalendarDatePicker::JBCalendarDatePicker::Product" /* JBCalendarDatePicker.framework */; 171 | productType = "com.apple.product-type.framework"; 172 | }; 173 | "JBCalendarDatePicker::JBCalendarDatePickerTests" /* JBCalendarDatePickerTests */ = { 174 | isa = PBXNativeTarget; 175 | buildConfigurationList = OBJ_52 /* Build configuration list for PBXNativeTarget "JBCalendarDatePickerTests" */; 176 | buildPhases = ( 177 | OBJ_55 /* Sources */, 178 | OBJ_58 /* Frameworks */, 179 | ); 180 | buildRules = ( 181 | ); 182 | dependencies = ( 183 | OBJ_60 /* PBXTargetDependency */, 184 | ); 185 | name = JBCalendarDatePickerTests; 186 | productName = JBCalendarDatePickerTests; 187 | productReference = "JBCalendarDatePicker::JBCalendarDatePickerTests::Product" /* JBCalendarDatePickerTests.xctest */; 188 | productType = "com.apple.product-type.bundle.unit-test"; 189 | }; 190 | "JBCalendarDatePicker::SwiftPMPackageDescription" /* JBCalendarDatePickerPackageDescription */ = { 191 | isa = PBXNativeTarget; 192 | buildConfigurationList = OBJ_41 /* Build configuration list for PBXNativeTarget "JBCalendarDatePickerPackageDescription" */; 193 | buildPhases = ( 194 | OBJ_44 /* Sources */, 195 | ); 196 | buildRules = ( 197 | ); 198 | dependencies = ( 199 | ); 200 | name = JBCalendarDatePickerPackageDescription; 201 | productName = JBCalendarDatePickerPackageDescription; 202 | productType = "com.apple.product-type.framework"; 203 | }; 204 | /* End PBXNativeTarget section */ 205 | 206 | /* Begin PBXProject section */ 207 | OBJ_1 /* Project object */ = { 208 | isa = PBXProject; 209 | attributes = { 210 | LastSwiftMigration = 9999; 211 | LastUpgradeCheck = 9999; 212 | }; 213 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "JBCalendarDatePicker" */; 214 | compatibilityVersion = "Xcode 3.2"; 215 | developmentRegion = en; 216 | hasScannedForEncodings = 0; 217 | knownRegions = ( 218 | en, 219 | ); 220 | mainGroup = OBJ_5 /* */; 221 | productRefGroup = OBJ_21 /* Products */; 222 | projectDirPath = ""; 223 | projectRoot = ""; 224 | targets = ( 225 | "JBCalendarDatePicker::JBCalendarDatePicker" /* JBCalendarDatePicker */, 226 | "JBCalendarDatePicker::SwiftPMPackageDescription" /* JBCalendarDatePickerPackageDescription */, 227 | "JBCalendarDatePicker::JBCalendarDatePickerPackageTests::ProductTarget" /* JBCalendarDatePickerPackageTests */, 228 | "JBCalendarDatePicker::JBCalendarDatePickerTests" /* JBCalendarDatePickerTests */, 229 | ); 230 | }; 231 | /* End PBXProject section */ 232 | 233 | /* Begin PBXSourcesBuildPhase section */ 234 | OBJ_31 /* Sources */ = { 235 | isa = PBXSourcesBuildPhase; 236 | buildActionMask = 0; 237 | files = ( 238 | OBJ_32 /* DateInputView.swift in Sources */, 239 | OBJ_33 /* Day.swift in Sources */, 240 | OBJ_34 /* JBCalendarDateCell.swift in Sources */, 241 | OBJ_35 /* JBCalendarViewController.swift in Sources */, 242 | OBJ_36 /* JBDatePicker.swift in Sources */, 243 | OBJ_37 /* JBDatePickerViewController.swift in Sources */, 244 | OBJ_38 /* UIColor+SystemAccent.swift in Sources */, 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | OBJ_44 /* Sources */ = { 249 | isa = PBXSourcesBuildPhase; 250 | buildActionMask = 0; 251 | files = ( 252 | OBJ_45 /* Package.swift in Sources */, 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | OBJ_55 /* Sources */ = { 257 | isa = PBXSourcesBuildPhase; 258 | buildActionMask = 0; 259 | files = ( 260 | OBJ_56 /* JBCalendarDatePickerTests.swift in Sources */, 261 | OBJ_57 /* XCTestManifests.swift in Sources */, 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | }; 265 | /* End PBXSourcesBuildPhase section */ 266 | 267 | /* Begin PBXTargetDependency section */ 268 | OBJ_50 /* PBXTargetDependency */ = { 269 | isa = PBXTargetDependency; 270 | target = "JBCalendarDatePicker::JBCalendarDatePickerTests" /* JBCalendarDatePickerTests */; 271 | targetProxy = "JBCalendarDatePicker::JBCalendarDatePickerTests" /* JBCalendarDatePickerTests */; 272 | }; 273 | OBJ_60 /* PBXTargetDependency */ = { 274 | isa = PBXTargetDependency; 275 | target = "JBCalendarDatePicker::JBCalendarDatePicker" /* JBCalendarDatePicker */; 276 | targetProxy = E4DA685325A9A5A900FE1035 /* PBXContainerItemProxy */; 277 | }; 278 | /* End PBXTargetDependency section */ 279 | 280 | /* Begin XCBuildConfiguration section */ 281 | OBJ_29 /* Debug */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ENABLE_TESTABILITY = YES; 285 | FRAMEWORK_SEARCH_PATHS = ( 286 | "$(inherited)", 287 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 288 | ); 289 | HEADER_SEARCH_PATHS = "$(inherited)"; 290 | INFOPLIST_FILE = JBCalendarDatePicker.xcodeproj/JBCalendarDatePicker_Info.plist; 291 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 292 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 293 | MACOSX_DEPLOYMENT_TARGET = 10.10; 294 | OTHER_CFLAGS = "$(inherited)"; 295 | OTHER_LDFLAGS = "$(inherited)"; 296 | OTHER_SWIFT_FLAGS = "$(inherited)"; 297 | PRODUCT_BUNDLE_IDENTIFIER = JBCalendarDatePicker; 298 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 299 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 300 | SKIP_INSTALL = YES; 301 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 302 | SWIFT_VERSION = 5.0; 303 | TARGET_NAME = JBCalendarDatePicker; 304 | TVOS_DEPLOYMENT_TARGET = 9.0; 305 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 306 | }; 307 | name = Debug; 308 | }; 309 | OBJ_3 /* Debug */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | CLANG_ENABLE_OBJC_ARC = YES; 313 | COMBINE_HIDPI_IMAGES = YES; 314 | COPY_PHASE_STRIP = NO; 315 | DEBUG_INFORMATION_FORMAT = dwarf; 316 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 317 | ENABLE_NS_ASSERTIONS = YES; 318 | GCC_OPTIMIZATION_LEVEL = 0; 319 | GCC_PREPROCESSOR_DEFINITIONS = ( 320 | "$(inherited)", 321 | "SWIFT_PACKAGE=1", 322 | "DEBUG=1", 323 | ); 324 | MACOSX_DEPLOYMENT_TARGET = 10.10; 325 | ONLY_ACTIVE_ARCH = YES; 326 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; 327 | PRODUCT_NAME = "$(TARGET_NAME)"; 328 | SDKROOT = macosx; 329 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 330 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG"; 331 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 332 | USE_HEADERMAP = NO; 333 | }; 334 | name = Debug; 335 | }; 336 | OBJ_30 /* Release */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ENABLE_TESTABILITY = YES; 340 | FRAMEWORK_SEARCH_PATHS = ( 341 | "$(inherited)", 342 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 343 | ); 344 | HEADER_SEARCH_PATHS = "$(inherited)"; 345 | INFOPLIST_FILE = JBCalendarDatePicker.xcodeproj/JBCalendarDatePicker_Info.plist; 346 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 347 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 348 | MACOSX_DEPLOYMENT_TARGET = 10.10; 349 | OTHER_CFLAGS = "$(inherited)"; 350 | OTHER_LDFLAGS = "$(inherited)"; 351 | OTHER_SWIFT_FLAGS = "$(inherited)"; 352 | PRODUCT_BUNDLE_IDENTIFIER = JBCalendarDatePicker; 353 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 354 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 355 | SKIP_INSTALL = YES; 356 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 357 | SWIFT_VERSION = 5.0; 358 | TARGET_NAME = JBCalendarDatePicker; 359 | TVOS_DEPLOYMENT_TARGET = 9.0; 360 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 361 | }; 362 | name = Release; 363 | }; 364 | OBJ_4 /* Release */ = { 365 | isa = XCBuildConfiguration; 366 | buildSettings = { 367 | CLANG_ENABLE_OBJC_ARC = YES; 368 | COMBINE_HIDPI_IMAGES = YES; 369 | COPY_PHASE_STRIP = YES; 370 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 371 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 372 | GCC_OPTIMIZATION_LEVEL = s; 373 | GCC_PREPROCESSOR_DEFINITIONS = ( 374 | "$(inherited)", 375 | "SWIFT_PACKAGE=1", 376 | ); 377 | MACOSX_DEPLOYMENT_TARGET = 10.10; 378 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; 379 | PRODUCT_NAME = "$(TARGET_NAME)"; 380 | SDKROOT = macosx; 381 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 382 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE"; 383 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 384 | USE_HEADERMAP = NO; 385 | }; 386 | name = Release; 387 | }; 388 | OBJ_42 /* Debug */ = { 389 | isa = XCBuildConfiguration; 390 | buildSettings = { 391 | LD = /usr/bin/true; 392 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk -package-description-version 5.3.0"; 393 | SWIFT_VERSION = 5.0; 394 | }; 395 | name = Debug; 396 | }; 397 | OBJ_43 /* Release */ = { 398 | isa = XCBuildConfiguration; 399 | buildSettings = { 400 | LD = /usr/bin/true; 401 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk -package-description-version 5.3.0"; 402 | SWIFT_VERSION = 5.0; 403 | }; 404 | name = Release; 405 | }; 406 | OBJ_48 /* Debug */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | }; 410 | name = Debug; 411 | }; 412 | OBJ_49 /* Release */ = { 413 | isa = XCBuildConfiguration; 414 | buildSettings = { 415 | }; 416 | name = Release; 417 | }; 418 | OBJ_53 /* Debug */ = { 419 | isa = XCBuildConfiguration; 420 | buildSettings = { 421 | CLANG_ENABLE_MODULES = YES; 422 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 423 | FRAMEWORK_SEARCH_PATHS = ( 424 | "$(inherited)", 425 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 426 | ); 427 | HEADER_SEARCH_PATHS = "$(inherited)"; 428 | INFOPLIST_FILE = JBCalendarDatePicker.xcodeproj/JBCalendarDatePickerTests_Info.plist; 429 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 430 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; 431 | MACOSX_DEPLOYMENT_TARGET = 10.15; 432 | OTHER_CFLAGS = "$(inherited)"; 433 | OTHER_LDFLAGS = "$(inherited)"; 434 | OTHER_SWIFT_FLAGS = "$(inherited)"; 435 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 436 | SWIFT_VERSION = 5.0; 437 | TARGET_NAME = JBCalendarDatePickerTests; 438 | TVOS_DEPLOYMENT_TARGET = 9.0; 439 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 440 | }; 441 | name = Debug; 442 | }; 443 | OBJ_54 /* Release */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | CLANG_ENABLE_MODULES = YES; 447 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 448 | FRAMEWORK_SEARCH_PATHS = ( 449 | "$(inherited)", 450 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 451 | ); 452 | HEADER_SEARCH_PATHS = "$(inherited)"; 453 | INFOPLIST_FILE = JBCalendarDatePicker.xcodeproj/JBCalendarDatePickerTests_Info.plist; 454 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 455 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; 456 | MACOSX_DEPLOYMENT_TARGET = 10.15; 457 | OTHER_CFLAGS = "$(inherited)"; 458 | OTHER_LDFLAGS = "$(inherited)"; 459 | OTHER_SWIFT_FLAGS = "$(inherited)"; 460 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 461 | SWIFT_VERSION = 5.0; 462 | TARGET_NAME = JBCalendarDatePickerTests; 463 | TVOS_DEPLOYMENT_TARGET = 9.0; 464 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 465 | }; 466 | name = Release; 467 | }; 468 | /* End XCBuildConfiguration section */ 469 | 470 | /* Begin XCConfigurationList section */ 471 | OBJ_2 /* Build configuration list for PBXProject "JBCalendarDatePicker" */ = { 472 | isa = XCConfigurationList; 473 | buildConfigurations = ( 474 | OBJ_3 /* Debug */, 475 | OBJ_4 /* Release */, 476 | ); 477 | defaultConfigurationIsVisible = 0; 478 | defaultConfigurationName = Release; 479 | }; 480 | OBJ_28 /* Build configuration list for PBXNativeTarget "JBCalendarDatePicker" */ = { 481 | isa = XCConfigurationList; 482 | buildConfigurations = ( 483 | OBJ_29 /* Debug */, 484 | OBJ_30 /* Release */, 485 | ); 486 | defaultConfigurationIsVisible = 0; 487 | defaultConfigurationName = Release; 488 | }; 489 | OBJ_41 /* Build configuration list for PBXNativeTarget "JBCalendarDatePickerPackageDescription" */ = { 490 | isa = XCConfigurationList; 491 | buildConfigurations = ( 492 | OBJ_42 /* Debug */, 493 | OBJ_43 /* Release */, 494 | ); 495 | defaultConfigurationIsVisible = 0; 496 | defaultConfigurationName = Release; 497 | }; 498 | OBJ_47 /* Build configuration list for PBXAggregateTarget "JBCalendarDatePickerPackageTests" */ = { 499 | isa = XCConfigurationList; 500 | buildConfigurations = ( 501 | OBJ_48 /* Debug */, 502 | OBJ_49 /* Release */, 503 | ); 504 | defaultConfigurationIsVisible = 0; 505 | defaultConfigurationName = Release; 506 | }; 507 | OBJ_52 /* Build configuration list for PBXNativeTarget "JBCalendarDatePickerTests" */ = { 508 | isa = XCConfigurationList; 509 | buildConfigurations = ( 510 | OBJ_53 /* Debug */, 511 | OBJ_54 /* Release */, 512 | ); 513 | defaultConfigurationIsVisible = 0; 514 | defaultConfigurationName = Release; 515 | }; 516 | /* End XCConfigurationList section */ 517 | }; 518 | rootObject = OBJ_1 /* Project object */; 519 | } 520 | -------------------------------------------------------------------------------- /JBCalendarDatePicker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /JBCalendarDatePicker.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JBCalendarDatePicker.xcodeproj/xcuserdata/compc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /JBCalendarDatePicker.xcodeproj/xcuserdata/compc.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | JBCalendarDatePicker.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | JBDatePicker.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Joshua Birnholz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 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: "JBCalendarDatePicker", 8 | platforms: [ 9 | .iOS(.v13), 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "JBCalendarDatePicker", 15 | targets: ["JBCalendarDatePicker"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "JBCalendarDatePicker", 26 | dependencies: [], 27 | path: "Sources", 28 | exclude: ["Info.plist"], 29 | resources: [ 30 | .process("JBCalendarDatePicker/JBDatePickerViewController.xib"), 31 | .process("JBCalendarDatePicker/JBCalendarViewController.xib"), 32 | .process("JBCalendarDatePicker/JBCalendarDateCell.xib"), 33 | ] 34 | ), 35 | .testTarget( 36 | name: "JBCalendarDatePickerTests", 37 | dependencies: ["JBCalendarDatePicker"], 38 | path: "Tests" 39 | ), 40 | ], 41 | swiftLanguageVersions: [.v5] 42 | ) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # JBCalendarDatePicker 3 | A replacement for UIDatePicker made for Catalyst. 4 | 5 | This is still a work in progress, there are bugs, and although it's written to work with different calendar systems and locales, it's not guaranteed to work correctly with everything! 6 | 7 | ![JBCalendarDatePicker](https://i.imgur.com/XusV7dx.gif) 8 | 9 | ## Installation 10 | 11 | To install as SPM, Go to: 12 | `Xcode -> File -> Swift Packages -> Add Package Dependency` 13 | 14 | Then enter this URL: 15 | `https://github.com/mohitnandwani/JBCalendarDatePicker.git` 16 | 17 | To install, add the source to the top of your podfile: 18 | 19 | `source 'https://github.com/joshbirnholz/JBPodSpecs.git'` 20 | 21 | Then add this pod to your targets: 22 | 23 | `pod 'JBCalendarDatePicker'` 24 | 25 | ## Use 26 | 27 | There are two classes you can use: `JBDatePickerViewController` and `JBCalendarViewController`. 28 | 29 | They are both similar to `UIDatePicker`, and their `date`, `minimumDate`, `maximumDate`, `calendar`, and `locale` properties can be configured in the same way. Configure them before presenting either of the view controllers. 30 | 31 | `JBDatePickerViewController` also has a `datePickerMode` property, although `UIDatePicker.Mode.countDownTimer` is not supported. 32 | 33 | ### JBDatePickerViewController 34 | 35 | ![JBDatePickerViewController](https://i.imgur.com/OtPr5V7.png) 36 | 37 | `JBDatePickerViewController` displays labels showing its represented date and allows the user to use the keyboard to enter a date. When the user clicks on the date portion, the view controller presents its own `JBCalendarViewController`. You can allow the user to select a date, time, or both, by setting the `datePickerMode` property. 38 | 39 | ```Swift 40 | import JBCalendarDatePicker 41 | 42 | class ViewController: UIViewController { 43 | 44 | var datePicker: JBDatePickerViewController! 45 | 46 | override func viewDidLoad() { 47 | super.viewDidLoad() 48 | 49 | let datePicker = JBDatePickerViewController() 50 | view.addSubview(datePicker.view) 51 | addChild(datePicker) 52 | datePicker.didMove(toParent: self) 53 | self.datePicker = datePicker 54 | 55 | // Configure the datePicker's properties 56 | } 57 | } 58 | ``` 59 | 60 | Or use it from a storyboard. Drag a Container View onto your storyboard. Change the view controller's class to `JBDatePickerViewController`. Give the embed segue an identifier, and then capture a reference to it: 61 | 62 | ```Swift 63 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 64 | if segue.identifier == "Embed Date Picker", let destination = segue.destination as? JBDatePickerViewController { 65 | self.datePicker = destination 66 | 67 | // Configure the datePicker's properties 68 | } 69 | } 70 | ``` 71 | 72 | ### JBCalendarViewController 73 | 74 | ![JBCalendarViewController](https://i.imgur.com/NV48jUk.png) 75 | 76 | `JBCalendarViewController` is just the calendar, without the labels. 77 | 78 | The view controller tries to present itself as a popover automatically, so be sure to set the `popoverPresentationController`'s `barButtonItem` property or the `sourceView` and `sourceRect` properties. 79 | 80 | ```Swift 81 | @IBOutlet func buttonPressed(_ sender: UIBarButtonItem) { 82 | let calendarPicker = JBCalendarViewController() 83 | calendarPicker.popoverPresentationController?.barButtonItem = sender 84 | 85 | // Configure the calendar's properties 86 | 87 | present(calendarPicker, animated: true, completion: nil) 88 | } 89 | ``` 90 | There is also a `JBCalendarViewControllerDelegate` protocol. 91 | 92 | ```Swift 93 | public protocol JBCalendarViewControllerDelegate: class { 94 | func calendarViewControllerDateChanged(_ calendarViewController: JBCalendarViewController) 95 | func calendarViewControllerWillDismiss(_ calendarViewController: JBCalendarViewController) 96 | func calendarViewControllerDidDismiss(_ calendarViewController: JBCalendarViewController) 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /Sources/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 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/CalendarDatePickerViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarDatePickerViewController.h 3 | // CalendarDatePickerViewController 4 | // 5 | // Created by Josh Birnholz on 28/10/2019. 6 | // Copyright © 2019 Josh Birnholz. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for CalendarDatePickerViewController. 12 | FOUNDATION_EXPORT double CalendarDatePickerViewControllerVersionNumber; 13 | 14 | //! Project version string for CalendarDatePickerViewController. 15 | FOUNDATION_EXPORT const unsigned char CalendarDatePickerViewControllerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/DateInputView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateInputView.swift 3 | // CalendarDatePickerViewController 4 | // 5 | // Created by Josh Birnholz on 10/29/19. 6 | // Copyright © 2019 Josh Birnholz. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | protocol DateInputViewDelegate: UIResponder, UIKeyInput { 13 | 14 | } 15 | 16 | class DateInputView: UIView, UIKeyInput { 17 | 18 | weak var delegate: DateInputViewDelegate? 19 | 20 | override func becomeFirstResponder() -> Bool { 21 | print(type(of: self), #function) 22 | let value = super.becomeFirstResponder() 23 | print(type(of: self), "is first responder: \(self.isFirstResponder)") 24 | return value 25 | } 26 | 27 | override func resignFirstResponder() -> Bool { 28 | print(type(of: self), #function) 29 | return delegate?.resignFirstResponder() ?? super.resignFirstResponder() 30 | } 31 | 32 | override var canBecomeFirstResponder: Bool { 33 | return true 34 | } 35 | 36 | // MARK: UIKeyInput 37 | 38 | var hasText: Bool { 39 | return delegate?.hasText ?? false 40 | } 41 | 42 | func insertText(_ text: String) { 43 | delegate?.insertText(text) 44 | } 45 | 46 | func deleteBackward() { 47 | delegate?.deleteBackward() 48 | } 49 | 50 | // MARK: UITextInputTraits 51 | 52 | // this doesn't seem to work for some reason. 53 | private var keyboardType: UIKeyboardType { 54 | return .numberPad 55 | } 56 | 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/Day.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Day.swift 3 | // CalendarDatePickerViewController 4 | // 5 | // Created by Josh Birnholz on 28/10/2019. 6 | // Copyright © 2019 Josh Birnholz. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Day: Equatable, Hashable { 12 | var calendar: Calendar 13 | var day: Int 14 | var month: Int 15 | var year: Int 16 | 17 | var date: Date { 18 | DateComponents(calendar: calendar, year: year, month: month, day: day).date! 19 | } 20 | 21 | var isToday: Bool { 22 | let todayComponents = calendar.dateComponents([.year, .month, .day], from: Date()) 23 | var components = DateComponents(calendar: calendar, year: year, month: month, day: day) 24 | let date = calendar.date(from: components)! 25 | components = calendar.dateComponents([.year, .month, .day], from: date) 26 | return todayComponents.day == components.day && todayComponents.month == components.month && todayComponents.year == components.year 27 | } 28 | 29 | static func == (lhs: Day, rhs: Day) -> Bool { 30 | if lhs.day == rhs.day && lhs.month == rhs.month && lhs.year == rhs.year { 31 | return true 32 | } 33 | return lhs.date == rhs.date 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/JBCalendarDateCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarDateCollectionViewCell.swift 3 | // CalendarDatePickerViewController 4 | // 5 | // Created by Josh Birnholz on 28/10/2019. 6 | // Copyright © 2019 Josh Birnholz. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | class JBCalendarDateCell: UICollectionViewCell { 13 | @IBOutlet weak var label: UILabel! 14 | } 15 | #endif 16 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/JBCalendarDateCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/JBCalendarDatePicker.h: -------------------------------------------------------------------------------- 1 | // 2 | // JBDatePicker.h 3 | // JBDatePicker 4 | // 5 | // Created by Josh Birnholz on 10/30/19. 6 | // Copyright © 2019 Josh Birnholz. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for JBDatePicker. 12 | FOUNDATION_EXPORT double JBDatePickerVersionNumber; 13 | 14 | //! Project version string for JBDatePicker. 15 | FOUNDATION_EXPORT const unsigned char JBDatePickerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/JBCalendarViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarDatePickerViewController.swift 3 | // Calendar Picker 4 | // 5 | // Created by Josh Birnholz on 10/27/19. 6 | // Copyright © 2019 Josh Birnholz. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | @objc public protocol JBCalendarViewControllerDelegate: class { 13 | func calendarViewControllerDateChanged(_ calendarViewController: JBCalendarViewController) 14 | func calendarViewControllerWillDismiss(_ calendarViewController: JBCalendarViewController) 15 | func calendarViewControllerDidDismiss(_ calendarViewController: JBCalendarViewController) 16 | } 17 | 18 | public class JBCalendarViewController: UIViewController, JBDatePicker { 19 | 20 | @objc public weak var delegate: JBCalendarViewControllerDelegate? 21 | 22 | @IBOutlet private weak var monthLabel: UILabel! 23 | @IBOutlet private weak var collectionView: UICollectionView! 24 | 25 | @IBOutlet private var weekSymbolLabels: [UILabel]! 26 | 27 | /// This property always returns `UIDatePicker.Mode.date`. Setting this property to a new value does nothing. It is not possible to change the date picker mode of the calendar interface. 28 | @objc public var datePickerMode: UIDatePicker.Mode { 29 | get { 30 | return .date 31 | } 32 | set { 33 | 34 | } 35 | } 36 | 37 | @objc public var calendar: Calendar! = Calendar.current { 38 | didSet { 39 | if calendar == nil { 40 | calendar = .current 41 | } 42 | updateWeekLabels() 43 | } 44 | } 45 | 46 | @objc public var locale: Locale? = .current { 47 | didSet { 48 | calendar.locale = locale 49 | } 50 | } 51 | 52 | @objc public var date: Date = Date() { 53 | didSet { 54 | switch (minimumDate, maximumDate) { 55 | case(let minimumDate?, let maximumDate?) where minimumDate < maximumDate : 56 | date = min(max(date, minimumDate), maximumDate) 57 | case (let minimumDate?, nil): 58 | date = max(date, minimumDate) 59 | case (nil, let maximumDate?): 60 | date = min(date, maximumDate) 61 | default: 62 | break 63 | } 64 | 65 | if current != nil { 66 | let components = calendar.dateComponents([.month, .year], from: date) 67 | 68 | if components.month! != current.month || components.year! != current.year { 69 | (current.month, current.year) = (components.month!, components.year!) 70 | } else { 71 | collectionView?.reloadData() 72 | } 73 | } 74 | 75 | delegate?.calendarViewControllerDateChanged(self) 76 | } 77 | } 78 | 79 | @objc public var minimumDate: Date? { 80 | didSet { 81 | collectionView?.reloadData() 82 | } 83 | } 84 | @objc public var maximumDate: Date? { 85 | didSet { 86 | collectionView?.reloadData() 87 | } 88 | } 89 | 90 | private var usableMinimumDate: Date? { 91 | if let minimumDate = minimumDate { 92 | if let maximumDate = maximumDate { 93 | if minimumDate < maximumDate { 94 | return minimumDate 95 | } else { 96 | return nil 97 | } 98 | } 99 | return minimumDate 100 | } 101 | 102 | return nil 103 | } 104 | 105 | private var usableMaximumDate: Date? { 106 | if let maximumDate = maximumDate { 107 | if let minimumDate = minimumDate { 108 | if minimumDate < maximumDate { 109 | return maximumDate 110 | } else { 111 | return nil 112 | } 113 | } 114 | return maximumDate 115 | } 116 | 117 | return nil 118 | } 119 | 120 | private var selectedDay: Day { 121 | let components = calendar.dateComponents([.day, .month, .year], from: date) 122 | return Day(calendar: calendar, day: components.day!, month: components.month!, year: components.year!) 123 | } 124 | 125 | private struct Current { 126 | var month: Int { 127 | didSet { 128 | let firstOfMonth = DateComponents(calendar: calendar, year: year, month: month, day: 1).date! 129 | let range = calendar.range(of: .month, in: .year, for: firstOfMonth)! 130 | if month > range.last! { 131 | month = range.first! 132 | year += 1 133 | } else if month < range.first! { 134 | month = range.last! 135 | year -= 1 136 | } 137 | } 138 | } 139 | 140 | var year: Int 141 | 142 | private let calendar: Calendar 143 | 144 | init(calendar: Calendar, month: Int, year: Int) { 145 | self.calendar = calendar 146 | self.month = month 147 | self.year = year 148 | } 149 | 150 | 151 | } 152 | 153 | private var current: Current! { 154 | didSet { 155 | updateMonthLabel() 156 | updateDays() 157 | collectionView.reloadData() 158 | } 159 | } 160 | 161 | private var days: [Day] = [] 162 | 163 | public required init?(coder: NSCoder) { 164 | super.init(nibName: "JBCalendarViewController", bundle: Bundle(for: Self.self)) 165 | commonInit() 166 | } 167 | 168 | public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 169 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 170 | commonInit() 171 | } 172 | 173 | public init() { 174 | super.init(nibName: "JBCalendarViewController", bundle: Bundle(for: Self.self)) 175 | commonInit() 176 | } 177 | 178 | private func commonInit() { 179 | modalPresentationStyle = .popover 180 | popoverPresentationController?.delegate = self 181 | preferredContentSize = CGSize(width: 200, height: 210) 182 | } 183 | 184 | override public func viewDidLoad() { 185 | super.viewDidLoad() 186 | 187 | calendar.locale = self.locale 188 | 189 | collectionView.delegate = self 190 | collectionView.dataSource = self 191 | collectionView.register(UINib(nibName: "JBCalendarDateCell", bundle: Bundle(for: Self.self)), forCellWithReuseIdentifier: "DateCell") 192 | 193 | #if targetEnvironment(macCatalyst) 194 | view.tintColor = .systemAccent 195 | #endif 196 | 197 | let selectedComponents = calendar.dateComponents([.year, .month], from: date) 198 | current = Current(calendar: calendar, month: selectedComponents.month!, year: selectedComponents.year!) 199 | 200 | let pan = UIPanGestureRecognizer(target: self, action: #selector(didPan(toSelectCells:))) 201 | collectionView.addGestureRecognizer(pan) 202 | 203 | let prevLong = UILongPressGestureRecognizer(target: self, action: #selector(previousMonthButtonTouchDown(_:))) 204 | prevLong.minimumPressDuration = 0 205 | 206 | } 207 | 208 | private func updateWeekLabels() { 209 | var symbols = calendar.veryShortStandaloneWeekdaySymbols 210 | if (calendar.locale ?? .current).languageCode == "en" { 211 | symbols = calendar.shortStandaloneWeekdaySymbols.map { String($0.prefix(2)) } 212 | } 213 | guard isViewLoaded else { return } 214 | for (index, symbol) in symbols.enumerated() { 215 | weekSymbolLabels[index].text = symbol 216 | } 217 | } 218 | 219 | private func updateDays() { 220 | let components = DateComponents(calendar: calendar, year: current.year, month: current.month) 221 | let date = components.date! 222 | 223 | let range = calendar.range(of: .day, in: .month, for: date)! 224 | 225 | days = range.map { Day(calendar: calendar, day: $0, month: current.month, year: current.year) } 226 | 227 | let startDate = calendar.dateInterval(of: .month, for: date)!.start 228 | let weekday = calendar.component(.weekday, from: startDate) 229 | 230 | let firstDay = days.first! 231 | for i in 0 ..< weekday-1 { 232 | var day = firstDay 233 | day.day -= i+1 234 | days.insert(day, at: 0) 235 | } 236 | 237 | let lastDay = days.last! 238 | let count = calendar.weekdaySymbols.count * 6 239 | for i in 0 ..< (count-days.count) { 240 | var day = lastDay 241 | day.day += i+1 242 | days.append(day) 243 | } 244 | } 245 | 246 | private func updateMonthLabel() { 247 | guard isViewLoaded else { return } 248 | 249 | let formatter = DateFormatter() 250 | formatter.locale = calendar.locale 251 | formatter.setLocalizedDateFormatFromTemplate("MMM yyyy") 252 | let components = DateComponents(calendar: calendar, year: current.year, month: current.month) 253 | monthLabel.text = formatter.string(from: components.date!) 254 | } 255 | 256 | @IBAction private func previousMonthButtonTouchUp(_ sender: Any) { 257 | timer?.invalidate() 258 | timer = nil 259 | } 260 | 261 | @IBAction private func selectedDayButtonPressed(_ sender: Any) { 262 | let components = calendar.dateComponents([.month, .year], from: date) 263 | let month = components.month! 264 | let year = components.year! 265 | current = Current(calendar: calendar, month: month, year: year) 266 | } 267 | 268 | @IBAction private func nextMonthButtonTouchUp(_ sender: Any) { 269 | timer?.invalidate() 270 | timer = nil 271 | } 272 | 273 | @IBAction private func previousMonthButtonTouchDown(_ sender: Any) { 274 | startRepeatingTimer { [weak self] in 275 | self?.current.month -= 1 276 | } 277 | } 278 | 279 | private var timer: Timer? 280 | private func startRepeatingTimer(_ action: @escaping () -> Void) { 281 | action() 282 | 283 | timer = Timer(fire: Date().addingTimeInterval(0.5), interval: 0.25, repeats: true) { timer in 284 | action() 285 | } 286 | RunLoop.main.add(timer!, forMode: .common) 287 | 288 | DispatchQueue.main.asyncAfter(deadline: .now() + 4) { 289 | if let timer = self.timer { 290 | timer.invalidate() 291 | self.timer = Timer(timeInterval: 0.075, repeats: true, block: { timer in 292 | action() 293 | }) 294 | RunLoop.main.add(self.timer!, forMode: .common) 295 | } 296 | } 297 | } 298 | 299 | @IBAction private func nextMonthButtonTouchDown(_ sender: Any) { 300 | startRepeatingTimer { [weak self] in 301 | self?.current.month += 1 302 | } 303 | } 304 | 305 | private var lastPanChangeDate: Date = Date() 306 | @objc private func didPan(toSelectCells panGesture: UIPanGestureRecognizer) { 307 | if panGesture.state == .began { 308 | collectionView.isUserInteractionEnabled = false 309 | } else if panGesture.state == .changed, let indexPath = collectionView.indexPathForItem(at: panGesture.location(in: collectionView)) { 310 | let day = days[indexPath.row] 311 | let date = DateComponents(calendar: calendar, year: current.year, month: current.month, day: day.day).date! 312 | let month = calendar.component(.month, from: date) 313 | 314 | if month == current.month || -lastPanChangeDate.timeIntervalSinceNow > 0.8 { 315 | self.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) 316 | self.collectionView(collectionView, didSelectItemAt: indexPath) 317 | 318 | lastPanChangeDate = Date() 319 | } 320 | } else if panGesture.state == .ended { 321 | collectionView.isUserInteractionEnabled = true 322 | } 323 | } 324 | 325 | } 326 | 327 | extension JBCalendarViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 328 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 329 | return days.count 330 | } 331 | 332 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 333 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DateCell", for: indexPath) as! JBCalendarDateCell 334 | let day = days[indexPath.row] 335 | 336 | let date = DateComponents(calendar: calendar, year: current.year, month: current.month, day: day.day).date! 337 | let components = calendar.dateComponents([.day, .month], from: date) 338 | 339 | cell.label.text = String(components.day!) 340 | cell.layer.cornerRadius = 4 341 | cell.layer.masksToBounds = true 342 | 343 | let isSelected = day == selectedDay 344 | 345 | let highlightedBackgroundColor: UIColor = day.isToday ? view.tintColor : .systemFill 346 | cell.backgroundColor = isSelected ? highlightedBackgroundColor : nil 347 | 348 | if day.isToday { 349 | if isSelected { 350 | cell.label.textColor = .lightLabel 351 | } else { 352 | cell.label.textColor = self.view.tintColor 353 | } 354 | } else if let minimumDate = usableMinimumDate, date < minimumDate { 355 | cell.label.textColor = .quaternaryLabel 356 | } else if let maximumDate = usableMaximumDate, date > maximumDate { 357 | cell.label.textColor = .quaternaryLabel 358 | } else if components.month == current.month { 359 | cell.label.textColor = .label 360 | } else { 361 | cell.label.textColor = .tertiaryLabel 362 | } 363 | 364 | return cell 365 | } 366 | 367 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 368 | 369 | let numberOfItems = collectionView.numberOfItems(inSection: 0) 370 | 371 | let spacing = (collectionViewLayout as! UICollectionViewFlowLayout).minimumInteritemSpacing * CGFloat(numberOfItems-1) 372 | let width = (self.collectionView.frame.width - spacing) / CGFloat(calendar.weekdaySymbols.count) 373 | let height = (self.collectionView.frame.height - spacing) / CGFloat(6) 374 | 375 | return CGSize(width: width, height: height) 376 | } 377 | 378 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 379 | let day = days[indexPath.row] 380 | 381 | var components = calendar.dateComponents([.timeZone, .year, .month, .day, .hour, .minute, .second, .nanosecond], from: self.date) 382 | components.day = day.day 383 | components.month = day.month 384 | components.year = day.year 385 | let newDate = calendar.date(from: components)! 386 | 387 | if let minimumDate = usableMinimumDate, newDate < minimumDate { 388 | return 389 | } else if let maximumDate = usableMaximumDate, newDate > maximumDate { 390 | return 391 | } 392 | 393 | collectionView.reloadData() 394 | 395 | self.date = newDate 396 | } 397 | 398 | public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { 399 | collectionView.reloadData() 400 | } 401 | 402 | } 403 | 404 | extension JBCalendarViewController: UIPopoverPresentationControllerDelegate { 405 | public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { 406 | return .none 407 | } 408 | 409 | public func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { 410 | delegate?.calendarViewControllerWillDismiss(self) 411 | print("final date:", date) 412 | } 413 | 414 | public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { 415 | delegate?.calendarViewControllerDidDismiss(self) 416 | } 417 | } 418 | #endif 419 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/JBCalendarViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 41 | 42 | 43 | 44 | 55 | 64 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 88 | 94 | 100 | 106 | 112 | 118 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/JBDatePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JBDatePicker.swift 3 | // CalendarDatePickerViewController 4 | // 5 | // Created by Josh Birnholz on 10/29/19. 6 | // Copyright © 2019 Josh Birnholz. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | 13 | public protocol JBDatePicker: UIResponder { 14 | var date: Date { get set } 15 | var calendar: Calendar! { get set } 16 | var locale: Locale? { get set } 17 | var minimumDate: Date? { get set } 18 | var maximumDate: Date? { get set } 19 | var datePickerMode: UIDatePicker.Mode { get set } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/JBDatePickerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JBDatePickerViewController.swift 3 | // CalendarDatePickerViewController 4 | // 5 | // Created by Josh Birnholz on 28/10/2019. 6 | // Copyright © 2019 Josh Birnholz. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public class JBDatePickerViewController: UIViewController, DateInputViewDelegate, JBDatePicker { 13 | 14 | // MARK: Public interface 15 | 16 | private var keyboardType: UIKeyboardType { 17 | return .numberPad 18 | } 19 | 20 | /// Use this property to change the type of information displayed by the date picker. It determines whether the date picker allows selection of a date, a time, or both date and time. The default mode is `UIDatePicker.Mode.dateAndTime`. See `UIDatePicker.Mode` for a list of mode constants. 21 | /// 22 | /// Setting this property to `UIDatePicker.Mode.countDownTimer` has no effect; this date picker does not support the countdown timer mode. 23 | @objc public var datePickerMode: UIDatePicker.Mode = .dateAndTime { 24 | didSet { 25 | if datePickerMode == .countDownTimer { 26 | datePickerMode = oldValue 27 | } 28 | } 29 | } 30 | 31 | private var dateInputView: DateInputView! { 32 | return (view as! DateInputView) 33 | } 34 | 35 | public var calendar: Calendar! = Calendar.current { 36 | didSet { 37 | if calendar == nil { 38 | calendar = .current 39 | } 40 | } 41 | } 42 | 43 | @objc public var locale: Locale? = .current { 44 | didSet { 45 | calendar.locale = locale 46 | } 47 | } 48 | 49 | @objc public var date: Date = Date() { 50 | didSet { 51 | switch (minimumDate, maximumDate) { 52 | case(let minimumDate?, let maximumDate?) where minimumDate < maximumDate : 53 | date = min(max(date, minimumDate), maximumDate) 54 | case (let minimumDate?, nil): 55 | date = max(date, minimumDate) 56 | case (nil, let maximumDate?): 57 | date = min(date, maximumDate) 58 | default: 59 | break 60 | } 61 | updateLabelText() 62 | setTextInputString("", updatingLabel: false) 63 | print("date set to \(date)") 64 | isPM = (12...23).contains(calendar.component(.hour, from: date)) 65 | 66 | presentedCalendar?.delegate = nil 67 | presentedCalendar?.date = date 68 | presentedCalendar?.delegate = self 69 | } 70 | } 71 | 72 | @objc public var minimumDate: Date? { 73 | didSet { 74 | updateLabelText() 75 | } 76 | } 77 | @objc public var maximumDate: Date? { 78 | didSet { 79 | updateLabelText() 80 | } 81 | } 82 | 83 | private var usableMinimumDate: Date? { 84 | if let minimumDate = minimumDate { 85 | if let maximumDate = maximumDate { 86 | if minimumDate < maximumDate { 87 | return minimumDate 88 | } else { 89 | return nil 90 | } 91 | } 92 | return minimumDate 93 | } 94 | 95 | return nil 96 | } 97 | 98 | private var usableMaximumDate: Date? { 99 | if let maximumDate = maximumDate { 100 | if let minimumDate = minimumDate { 101 | if minimumDate < maximumDate { 102 | return maximumDate 103 | } else { 104 | return nil 105 | } 106 | } 107 | return maximumDate 108 | } 109 | 110 | return nil 111 | } 112 | 113 | fileprivate var _textInputString: String = "" 114 | fileprivate var textInputString: String { return _textInputString } 115 | 116 | fileprivate func setTextInputString(_ newValue: String, updatingLabel: Bool) { 117 | _textInputString = newValue 118 | if updatingLabel, let selectedDatePart = selectedDatePart { 119 | label(for: selectedDatePart).text = _textInputString 120 | } 121 | } 122 | 123 | // MARK: Init 124 | 125 | public required init?(coder: NSCoder) { 126 | super.init(nibName: "JBDatePickerViewController", bundle: Bundle(for: Self.self)) 127 | commonInit() 128 | } 129 | 130 | public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 131 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 132 | commonInit() 133 | } 134 | 135 | public init() { 136 | super.init(nibName: "JBDatePickerViewController", bundle: Bundle(for: Self.self)) 137 | commonInit() 138 | } 139 | 140 | private func commonInit() { 141 | 142 | } 143 | 144 | @IBOutlet private var labels: [UILabel]! 145 | @IBOutlet private var slashLabels: [UILabel]! 146 | @IBOutlet private weak var fullStackView: UIStackView! 147 | @IBOutlet private weak var datePartsStackView: UIStackView! 148 | @IBOutlet private weak var timePartsStackView: UIStackView! 149 | 150 | override public func viewDidLoad() { 151 | super.viewDidLoad() 152 | calendar.locale = locale ?? .current 153 | 154 | view.backgroundColor = .clear 155 | dateInputView.delegate = self 156 | 157 | #if targetEnvironment(macCatalyst) 158 | view.tintColor = .systemAccent 159 | #endif 160 | 161 | isPM = (12...23).contains(calendar.component(.hour, from: date)) 162 | 163 | datePartsStackView.isHidden = datePickerMode == .time 164 | timePartsStackView.isHidden = datePickerMode == .date 165 | 166 | setupTextFields() 167 | 168 | updateLabelText() 169 | 170 | } 171 | 172 | override public var canBecomeFirstResponder: Bool { 173 | return dateInputView.canBecomeFirstResponder 174 | } 175 | 176 | override public func becomeFirstResponder() -> Bool { 177 | print(type(of: self), #function) 178 | if selectedDatePart == nil { 179 | selectedDatePart = dateParts.first 180 | } 181 | return dateInputView.becomeFirstResponder() 182 | } 183 | 184 | override public func resignFirstResponder() -> Bool { 185 | print(type(of: self), #function) 186 | selectedDatePart = nil 187 | 188 | // if let presented = presentedCalendar { 189 | // dismiss(animated: true, completion: nil) 190 | // } 191 | 192 | return super.resignFirstResponder() 193 | } 194 | 195 | @objc private func tapGestureRecognized(_ sender: UITapGestureRecognizer) { 196 | guard let label = sender.view as? UILabel, let datePart = self.datePart(for: label) else { return } 197 | selectedDatePart = datePart 198 | _ = dateInputView.becomeFirstResponder() 199 | } 200 | 201 | private var isPM = false 202 | 203 | private enum DatePart: String, CaseIterable { 204 | case day = "dd" 205 | case month = "MM" 206 | case year = "yyyy" 207 | case hour12 = "h" 208 | case hour24 = "HH" 209 | case minute = "mm" 210 | case amPM = "a" 211 | 212 | func set(value: Int, of components: inout DateComponents, using calendar: Calendar, isPM: Bool) { 213 | switch self { 214 | case .day: 215 | components.setValue(value, for: .day) 216 | case .month: 217 | // TODO: Set day to last day of month when the date range for the new month doesn't include the old day. 218 | components.setValue(value, for: .month) 219 | case .year: 220 | components.setValue(value, for: .year) 221 | case .hour12: 222 | var value = value 223 | 224 | if value == 12 && !isPM { 225 | value = 0 226 | } else if (1...11).contains(value) && isPM { 227 | value += 12 228 | } 229 | 230 | components.setValue(value, for: .hour) 231 | case .hour24: 232 | components.setValue(value, for: .hour) 233 | case .minute: 234 | components.setValue(value, for: .minute) 235 | case .amPM: 236 | break 237 | } 238 | } 239 | 240 | func maxComponentLength(using calendar: Calendar) -> Int { 241 | if self == .amPM { 242 | return max(calendar.amSymbol.count, calendar.pmSymbol.count) 243 | } else if self == .hour12 { 244 | return 2 245 | } 246 | 247 | return rawValue.count 248 | } 249 | } 250 | 251 | private var presentedCalendar: JBCalendarViewController? { 252 | return presentedViewController as? JBCalendarViewController 253 | } 254 | 255 | private var selectedDatePart: DatePart? { 256 | didSet { 257 | for datePart in visibleDateParts { 258 | let label = self.label(for: datePart) 259 | let isSelected = selectedDatePart == datePart 260 | label.backgroundColor = isSelected ? view.tintColor : nil 261 | label.textColor = isSelected ? .lightLabel : .label 262 | } 263 | 264 | self.setTextInputString("", updatingLabel: false) 265 | 266 | guard let selectedDatePart = selectedDatePart else { 267 | // presentedCalendar?.dismiss(animated: true, completion: nil) 268 | return 269 | } 270 | 271 | if selectedDatePart == .day || selectedDatePart == .month || selectedDatePart == .year && presentedCalendar == nil { 272 | let calendarVC = JBCalendarViewController() 273 | calendarVC.date = date 274 | calendarVC.calendar = calendar 275 | calendarVC.locale = locale 276 | calendarVC.minimumDate = minimumDate 277 | calendarVC.maximumDate = maximumDate 278 | calendarVC.popoverPresentationController?.sourceView = datePartsStackView 279 | calendarVC.popoverPresentationController?.sourceRect = datePartsStackView.frame 280 | // calendarVC.popoverPresentationController?.sourceRect = dayLabel.frame 281 | calendarVC.popoverPresentationController?.permittedArrowDirections = [.up] 282 | calendarVC.popoverPresentationController?.passthroughViews = [fullStackView] 283 | calendarVC.delegate = self 284 | self.present(calendarVC, animated: true, completion: nil) 285 | } else { 286 | // presentedCalendar?.dismiss(animated: true, completion: nil) 287 | } 288 | } 289 | } 290 | 291 | private var dateParts: [DatePart]! { 292 | didSet { 293 | amPMLabel.isHidden = !dateParts.contains(.hour12) 294 | } 295 | } 296 | 297 | private var yearLabel: UILabel { 298 | let index = dateParts.firstIndex(of: .year)! 299 | return labels[index] 300 | } 301 | 302 | private var monthLabel: UILabel { 303 | let index = dateParts.firstIndex(of: .month)! 304 | return labels[index] 305 | } 306 | 307 | private var dayLabel: UILabel { 308 | let index = dateParts.firstIndex(of: .day)! 309 | return labels[index] 310 | } 311 | 312 | @IBOutlet private weak var hourLabel: UILabel! 313 | @IBOutlet private weak var minuteLabel: UILabel! 314 | @IBOutlet private weak var amPMLabel: UILabel! 315 | 316 | private func setupTextFields() { 317 | var allLabels = labels ?? [] 318 | allLabels.append(hourLabel) 319 | allLabels.append(minuteLabel) 320 | allLabels.append(amPMLabel) 321 | for label in allLabels { 322 | label.font = UIFont.monospacedDigitSystemFont(ofSize: label.font!.pointSize, weight: .regular) 323 | label.sizeToFit() 324 | NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: label.frame.size.width).isActive = true 325 | 326 | label.layer.masksToBounds = true 327 | label.layer.cornerRadius = 4 328 | label.backgroundColor = .clear 329 | label.textColor = .label 330 | 331 | let gesture = UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:))) 332 | label.addGestureRecognizer(gesture) 333 | label.isUserInteractionEnabled = true 334 | } 335 | 336 | let template = dateTemplate 337 | let dateFormat = DateFormatter.dateFormat(fromTemplate: template, options: 0, locale: locale ?? .current)! 338 | let components = dateFormat.split(maxSplits: .max, omittingEmptySubsequences: true, whereSeparator: { character -> Bool in 339 | !template.contains(character.lowercased()) 340 | }) 341 | dateParts = components.compactMap { DatePart(rawValue: String($0)) } 342 | } 343 | 344 | private let dateTemplate: String = "MMddyyyyhmma" 345 | 346 | private let formatter = DateFormatter() 347 | 348 | private var visibleDateParts: [DatePart] { 349 | let allowedDateParts: Set = { 350 | switch datePickerMode { 351 | case .time: 352 | if dateParts.contains(.hour12) { 353 | return [.hour12, .minute, .amPM] 354 | } else { 355 | return [.hour24, .minute] 356 | } 357 | case .date: 358 | return [.year, .month, .day] 359 | default: 360 | var returnValue: Set = [.year, .month, .day] 361 | if dateParts.contains(.hour12) { 362 | returnValue.insert(.hour12) 363 | returnValue.insert(.minute) 364 | returnValue.insert(.amPM) 365 | } else { 366 | returnValue.insert(.hour24) 367 | returnValue.insert(.minute) 368 | } 369 | return returnValue 370 | } 371 | }() 372 | 373 | return dateParts.filter { allowedDateParts.contains($0) } 374 | } 375 | 376 | // private var labelsAndDateParts: [(UILabel, DatePart)] { 377 | // return visibleDateParts.map { (self.label(for: $0), $0) } 378 | // } 379 | 380 | private func label(for datePart: DatePart) -> UILabel { 381 | switch datePart { 382 | case .day: return dayLabel 383 | case .month: return monthLabel 384 | case .year: return yearLabel 385 | case .hour12: return hourLabel 386 | case .hour24: return hourLabel 387 | case .minute: return minuteLabel 388 | case .amPM: return amPMLabel 389 | } 390 | } 391 | 392 | private func datePart(for label: UILabel) -> DatePart? { 393 | switch label { 394 | case yearLabel: return .year 395 | case monthLabel: return .month 396 | case dayLabel: return .day 397 | case hourLabel: 398 | if dateParts.contains(.hour12) { 399 | return .hour12 400 | } 401 | return .hour24 402 | case minuteLabel: return .minute 403 | case amPMLabel: return .amPM 404 | default: return nil 405 | } 406 | } 407 | 408 | private func updateLabelText() { 409 | for datePart in visibleDateParts { 410 | formatter.dateFormat = datePart.rawValue 411 | label(for: datePart).text = String(formatter.string(from: date).prefix(datePart.maxComponentLength(using: calendar))) 412 | } 413 | 414 | formatter.dateFormat = "a" 415 | amPMLabel.text = formatter.string(from: date) 416 | } 417 | 418 | private var finalizeEditTimer: Timer? { 419 | didSet { 420 | oldValue?.invalidate() 421 | } 422 | } 423 | 424 | } 425 | 426 | extension JBDatePickerViewController: UIKeyInput { 427 | public var hasText: Bool { 428 | return !textInputString.isEmpty 429 | } 430 | 431 | fileprivate func selectNextDatePart() { 432 | guard let selectedDatePart = selectedDatePart else { return } 433 | if let index = visibleDateParts.lastIndex(of: selectedDatePart), visibleDateParts.indices.contains(index+1) { 434 | self.selectedDatePart = visibleDateParts[index+1] 435 | } else { 436 | self.selectedDatePart = visibleDateParts.first 437 | } 438 | } 439 | 440 | public func insertText(_ text: String) { 441 | guard let selectedDatePart = selectedDatePart else { return } 442 | 443 | if text == "\t" { 444 | finalize(datePart: selectedDatePart) 445 | 446 | selectNextDatePart() 447 | 448 | return 449 | } 450 | 451 | if selectedDatePart == .amPM { 452 | setTextInputString(text, updatingLabel: true) 453 | finalize(datePart: selectedDatePart) 454 | return 455 | } 456 | 457 | guard let proposedValue = Int(textInputString + text) else { return } 458 | 459 | let validValues: [Int] = { 460 | switch selectedDatePart { 461 | case .day: 462 | return calendar.range(of: .day, in: .month, for: date).map(Array.init) ?? [] 463 | case .month: 464 | return calendar.range(of: .month, in: .year, for: date).map(Array.init) ?? [] 465 | case .year: 466 | return Array(1...9999) 467 | case .hour12: 468 | return Array(1...12) 469 | case .hour24: 470 | return Array(0...23) 471 | case .minute: 472 | return Array(0...59) 473 | case .amPM: 474 | return [] 475 | } 476 | }() 477 | 478 | let valueIsValid: Bool = { 479 | return validValues.contains(proposedValue) 480 | }() 481 | 482 | guard valueIsValid else { return } 483 | 484 | setTextInputString(String(proposedValue), updatingLabel: true) 485 | 486 | if textInputString.count >= selectedDatePart.maxComponentLength(using: calendar) { 487 | finalize(datePart: selectedDatePart) 488 | } else { 489 | startFinalizeTimer(datePart: selectedDatePart) 490 | } 491 | 492 | } 493 | 494 | public func deleteBackward() { 495 | 496 | guard !textInputString.isEmpty else { return } 497 | var input = textInputString 498 | input.removeLast() 499 | setTextInputString(input, updatingLabel: true) 500 | 501 | if let selectedDatePart = selectedDatePart, !textInputString.isEmpty { 502 | startFinalizeTimer(datePart: selectedDatePart) 503 | } 504 | } 505 | 506 | private func startFinalizeTimer(datePart: DatePart) { 507 | finalizeEditTimer = Timer(timeInterval: 1, repeats: false) { timer in 508 | self.finalize(datePart: datePart) 509 | } 510 | RunLoop.main.add(finalizeEditTimer!, forMode: .common) 511 | } 512 | 513 | private func finalize(datePart: DatePart) { 514 | var components = calendar.dateComponents([.timeZone, .year, .month, .day, .hour, .minute, .second, .nanosecond], from: date) 515 | if let value = Int(textInputString) { 516 | datePart.set(value: value, of: &components, using: calendar, isPM: isPM) 517 | if let date = calendar.date(from: components) { 518 | self.date = date 519 | } else { 520 | updateLabelText() 521 | } 522 | } else if datePart == .amPM && !textInputString.isEmpty { 523 | if calendar.amSymbol.lowercased().hasPrefix(textInputString.lowercased()) && isPM { 524 | isPM = false 525 | print("setting to am") 526 | components.hour! -= 12 527 | } else if calendar.pmSymbol.lowercased().hasPrefix(textInputString.lowercased()) && !isPM { 528 | isPM = true 529 | print("setting to pm") 530 | components.hour! += 12 531 | } 532 | if let date = calendar.date(from: components) { 533 | self.date = date 534 | } else { 535 | updateLabelText() 536 | } 537 | } 538 | setTextInputString("", updatingLabel: false) 539 | finalizeEditTimer?.invalidate() 540 | } 541 | 542 | } 543 | 544 | extension JBDatePickerViewController: JBCalendarViewControllerDelegate { 545 | public func calendarViewControllerDateChanged(_ calendarViewController: JBCalendarViewController) { 546 | self.date = calendarViewController.date 547 | } 548 | 549 | public func calendarViewControllerWillDismiss(_ calendarViewController: JBCalendarViewController) { 550 | _ = dateInputView.resignFirstResponder() 551 | } 552 | 553 | public func calendarViewControllerDidDismiss(_ calendarViewController: JBCalendarViewController) { 554 | 555 | } 556 | } 557 | 558 | #endif 559 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/JBDatePickerViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 50 | 56 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 88 | 94 | 95 | 96 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /Sources/JBCalendarDatePicker/UIColor+SystemAccent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+SystemAccent.swift 3 | // CalendarDatePickerViewController 4 | // 5 | // Created by Josh Birnholz on 28/10/2019. 6 | // Copyright © 2019 Josh Birnholz. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | extension UIColor { 13 | 14 | #if targetEnvironment(macCatalyst) 15 | static var systemAccent: UIColor { 16 | let hasAccentSet = UserDefaults.standard.object(forKey: "AppleAccentColor") != nil 17 | let systemAccentColor = UserDefaults.standard.integer(forKey: "AppleAccentColor") 18 | var returnColor: UIColor = UIColor { traitCollection in 19 | traitCollection.userInterfaceStyle == .dark ? #colorLiteral(red: 0.008315349929, green: 0.3450804651, blue: 0.817365706, alpha: 1) : #colorLiteral(red: 0.01329958253, green: 0.3846624196, blue: 0.8779004216, alpha: 1) 20 | } 21 | if hasAccentSet { 22 | switch systemAccentColor { 23 | case -1: 24 | returnColor = UIColor { traitCollection in 25 | traitCollection.userInterfaceStyle == .dark ? #colorLiteral(red: 0.4039281607, green: 0.403850317, blue: 0.4124818146, alpha: 1) : #colorLiteral(red: 0.5019147992, green: 0.5019902587, blue: 0.5018982291, alpha: 1) 26 | } 27 | case 0: 28 | returnColor = UIColor { traitCollection in 29 | traitCollection.userInterfaceStyle == .dark ? #colorLiteral(red: 0.82002002, green: 0.2045214176, blue: 0.2204136252, alpha: 1) : #colorLiteral(red: 0.7370213866, green: 0.1443678439, blue: 0.1633504629, alpha: 1) 30 | } 31 | case 1: 32 | returnColor = UIColor { traitCollection in 33 | traitCollection.userInterfaceStyle == .dark ? #colorLiteral(red: 0.7512640357, green: 0.3605512679, blue: 0.01273573376, alpha: 1) : #colorLiteral(red: 0.8462041616, green: 0.4178547263, blue: 0.05405366421, alpha: 1) 34 | } 35 | case 2: 36 | returnColor = UIColor { traitCollection in 37 | traitCollection.userInterfaceStyle == .dark ? #colorLiteral(red: 0.8009095192, green: 0.5611655712, blue: 0.05494389683, alpha: 1) : #colorLiteral(red: 0.8690621257, green: 0.6199508309, blue: 0.07889743894, alpha: 1) 38 | } 39 | case 3: 40 | returnColor = UIColor { traitCollection in 41 | traitCollection.userInterfaceStyle == .dark ? #colorLiteral(red: 0.2549478412, green: 0.5663680434, blue: 0.1645001471, alpha: 1) : #colorLiteral(red: 0.3048421741, green: 0.6298194528, blue: 0.1963118315, alpha: 1) 42 | } 43 | case 5: 44 | returnColor = UIColor { traitCollection in 45 | traitCollection.userInterfaceStyle == .dark ? #colorLiteral(red: 0.500952661, green: 0.1951716244, blue: 0.5008149147, alpha: 1) : #colorLiteral(red: 0.4900261164, green: 0.1631549001, blue: 0.4976372719, alpha: 1) 46 | } 47 | case 6: 48 | returnColor = UIColor { traitCollection in 49 | traitCollection.userInterfaceStyle == .dark ? #colorLiteral(red: 0.7823504806, green: 0.1956582665, blue: 0.4722630978, alpha: 1) : #colorLiteral(red: 0.8491325974, green: 0.2301979959, blue: 0.5240355134, alpha: 1) 50 | } 51 | default: 52 | break 53 | } 54 | } 55 | return returnColor 56 | } 57 | #endif 58 | 59 | static let lightLabel = UIColor { traitCollection in 60 | if traitCollection.userInterfaceStyle == .dark { 61 | return .label 62 | } else { 63 | return .systemBackground 64 | } 65 | } 66 | } 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /Tests/JBCalendarDatePickerTests/JBCalendarDatePickerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import JBCalendarDatePicker 3 | 4 | final class JBCalendarDatePickerTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/JBCalendarDatePickerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(JBCalendarDatePickerTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------