├── .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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------