├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Images
├── compact-red-purple.png
├── custom-button.png
├── graphical-green-pink.png
└── wheel-orange-blue.png
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
└── Sources
└── SwiftUIDatePickerDialog
└── SwiftUIDatePickerDialog.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Images/compact-red-purple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/globulus/swiftui-date-picker-dialog/71fd42d30b6659dc66574db068ade4fefd4ced4e/Images/compact-red-purple.png
--------------------------------------------------------------------------------
/Images/custom-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/globulus/swiftui-date-picker-dialog/71fd42d30b6659dc66574db068ade4fefd4ced4e/Images/custom-button.png
--------------------------------------------------------------------------------
/Images/graphical-green-pink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/globulus/swiftui-date-picker-dialog/71fd42d30b6659dc66574db068ade4fefd4ced4e/Images/graphical-green-pink.png
--------------------------------------------------------------------------------
/Images/wheel-orange-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/globulus/swiftui-date-picker-dialog/71fd42d30b6659dc66574db068ade4fefd4ced4e/Images/wheel-orange-blue.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Gordan Glavaš
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "SwiftUIGenericDialog",
6 | "repositoryURL": "https://github.com/globulus/swiftui-generic-dialog",
7 | "state": {
8 | "branch": null,
9 | "revision": "e7f1a107014c3767c4220f9cae1e57d5c9f8fa8b",
10 | "version": "1.0.1"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/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: "SwiftUIDatePickerDialog",
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: "SwiftUIDatePickerDialog",
15 | targets: ["SwiftUIDatePickerDialog"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | .package(name: "SwiftUIGenericDialog", url: "https://github.com/globulus/swiftui-generic-dialog", from: "1.0.1")
21 | ],
22 | targets: [
23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
24 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
25 | .target(
26 | name: "SwiftUIDatePickerDialog",
27 | dependencies: ["SwiftUIGenericDialog"]),
28 | .testTarget(
29 | name: "SwiftUIDatePickerDialogTests",
30 | dependencies: ["SwiftUIDatePickerDialog"]),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUIDatePickerDialog
2 |
3 | **Fully-customizable SwiftUI date/time picker dialog**. Features include:
4 |
5 | * Optional dialog title.
6 | * Selected date binding.
7 | * Choose which picker component to display - date, time or both.
8 | * Supports all SwiftUI `DatePicker` styles - default, compact, wheel and graphical.
9 | * Specify valid range of selectable dates.
10 | * Fully cancellable - cancelation reverts date selection to the original value, from when the dialog was first displayed.
11 | * Set dialog background color.
12 | * Dialog-wide foreground color, affecting the title, date picker and buttons.
13 | * Fully customizable buttons - any number, any content, any action - with default confirmation and cancellation buttons.
14 |
15 | Examples of the dialog in action:
16 |
17 | * Graphical-style:
18 |
19 | 
20 |
21 | * Compact-style (default):
22 |
23 | 
24 |
25 | * Wheel dialog:
26 |
27 | 
28 |
29 | * Dialog with custom buttons:
30 |
31 | 
32 |
33 | ## Installation
34 |
35 | This component is distributed as a **Swift package**.
36 |
37 | ## Sample usage
38 |
39 | Use the `dateTimePickerDialog` modifier on the View on top of which you want to display the dialog:
40 |
41 | ```swift
42 | struct DateTimePickerDialogTest: View {
43 | @State private var isShowing = true
44 | @State private var date = Date()
45 | let style: DateTimePickerDialog.Style
46 | let displayComponents: DatePickerComponents
47 | private let backgroundColor = Color.blue.opacity(0.1)
48 | private let textColor = Color.orange
49 |
50 | var body: some View {
51 | List(1..<6) {
52 | Text("\($0)")
53 | .onTapGesture {
54 | self.isShowing = true
55 | }
56 | }.dateTimePickerDialog(isShowing: $isShowing,
57 | cancelOnTapOutside: true,
58 | selection: $date,
59 | displayComponents: displayComponents,
60 | style: style,
61 | backgroundColor: backgroundColor,
62 | foregroundColor: textColor,
63 | buttons: [
64 | .default(label: "Default"),
65 | .cancel(label: "Cancel"),
66 | .custom(label: { Text("Call mom").foregroundColor(.red) }, action: { dialog in
67 |
68 | })
69 | ]) {
70 | Text("Custom label")
71 | }
72 | }
73 | }
74 |
75 | struct DateTimePickerDialogWheel_Previews: PreviewProvider {
76 | static var previews: some View {
77 | DateTimePickerDialogTest(style: .wheel, displayComponents: [.date, .hourAndMinute])
78 | }
79 | }
80 |
81 | @available(iOS 14.0, *)
82 | struct DateTimePickerDialogCompact_Previews: PreviewProvider {
83 | static var previews: some View {
84 | DateTimePickerDialogTest(style: .compact, displayComponents: [.date])
85 | }
86 | }
87 | ```
88 |
--------------------------------------------------------------------------------
/Sources/SwiftUIDatePickerDialog/SwiftUIDatePickerDialog.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SwiftUIGenericDialog
3 |
4 | public struct DateTimePickerDialog: View {
5 | @Binding public var isShowing: Bool
6 | @Binding public var selection: Date
7 | public let range: ClosedRange
8 | public let components: DatePickerComponents
9 | public let style: Style
10 | public let backgroundColor: Color?
11 | public let foregroundColor: Color?
12 | public let buttons: [DateTimePickerDialog.Button]
13 | @ViewBuilder public let label: (() -> Text)?
14 |
15 | // this allows for cancellation to happen - any change in the DatePicker is
16 | // reflected in `internalSelection` and is only applied to `selection` when
17 | // `confirm` is called
18 | private var internalSelection: State
19 |
20 | init(isShowing: Binding,
21 | selection: Binding,
22 | range: ClosedRange,
23 | components: DatePickerComponents,
24 | style: Style,
25 | backgroundColor: Color?,
26 | foregroundColor: Color?,
27 | buttons: [DateTimePickerDialog.Button],
28 | label: (() -> Text)?) {
29 | _isShowing = isShowing
30 | _selection = selection
31 | self.range = range
32 | self.components = components
33 | self.style = style
34 | self.backgroundColor = backgroundColor
35 | self.foregroundColor = foregroundColor
36 | self.buttons = buttons
37 | self.label = label
38 | internalSelection = State(initialValue: selection.wrappedValue)
39 | }
40 |
41 | public var body: some View {
42 | VStack {
43 | if label != nil {
44 | label!()
45 | .foregroundColor(foregroundColor ?? .black)
46 | .padding(.bottom, 10)
47 | }
48 | datePicker
49 | HStack {
50 | ForEach(buttons) { button in
51 | button
52 | .swiftUIButton(in: self)
53 | .frame(minWidth: 0, maxWidth: .infinity) // evenly space buttons
54 | }
55 | }.frame(minWidth: 0, maxWidth: .infinity) // fill entire width of the dialog
56 | .padding(.top, 10)
57 | }
58 | .padding()
59 | .background(backgroundColor ?? .white)
60 | }
61 |
62 | private var datePicker: some View {
63 | let dp = DatePicker(selection: internalSelection.projectedValue,
64 | in: range,
65 | displayedComponents: components) {
66 | EmptyView()
67 | }.labelsHidden()
68 |
69 | var view: AnyView = AnyView(dp.datePickerStyle(DefaultDatePickerStyle()))
70 | if style == .wheel {
71 | view = AnyView(dp.datePickerStyle(WheelDatePickerStyle()))
72 | } else if #available(iOS 14.0, *) {
73 | if style == .compact {
74 | view = AnyView(dp.datePickerStyle(CompactDatePickerStyle()))
75 | } else if style == .graphical {
76 | view = AnyView(dp.datePickerStyle(GraphicalDatePickerStyle()))
77 | }
78 | }
79 |
80 | if let color = foregroundColor {
81 | if #available(iOS 14.0, *),
82 | style == .compact || style == .default {
83 | view = AnyView(view.accentColor(color))
84 | } else {
85 | view = AnyView(view
86 | .colorInvert()
87 | .colorMultiply(color))
88 | }
89 | }
90 |
91 | return view
92 | }
93 |
94 | public func confirm() {
95 | selection = internalSelection.wrappedValue
96 | isShowing = false
97 | }
98 |
99 | public func cancel() {
100 | isShowing = false
101 | }
102 |
103 | public static var defaultButtons: [DateTimePickerDialog.Button] {
104 | [.default(label: "OK", action: { dialog in
105 | dialog.confirm()
106 | }),
107 | .cancel(label: "Cancel", action: { dialog in
108 | dialog.cancel()
109 | })]
110 | }
111 |
112 | public enum Style {
113 | case `default`, wheel
114 |
115 | @available(iOS 14.0, *)
116 | case compact, graphical
117 | }
118 |
119 | public struct Button: Identifiable {
120 | public let id = UUID()
121 | let label: () -> Text
122 | let action: (DateTimePickerDialog) -> Void
123 |
124 | func swiftUIButton(in dialog: DateTimePickerDialog) -> SwiftUI.Button {
125 | SwiftUI.Button(action: { action(dialog) },
126 | label: { label()
127 | .foregroundColor(dialog.foregroundColor ?? .blue)
128 | })
129 | }
130 |
131 | public static func `default`(label: String,
132 | action: ((DateTimePickerDialog) -> Void)? = nil) -> Button {
133 | Button(label: { Text(label)
134 | .fontWeight(.bold) },
135 | action: action ?? { $0.confirm() })
136 | }
137 |
138 | public static func cancel(label: String,
139 | action: ((DateTimePickerDialog) -> Void)? = nil) -> Button {
140 | Button(label: { Text(label) },
141 | action: action ?? { $0.cancel() })
142 | }
143 |
144 | public static func custom(label: @escaping () -> Text,
145 | action: @escaping (DateTimePickerDialog) -> Void) -> Button {
146 | Button(label: label, action: action)
147 | }
148 | }
149 | }
150 |
151 | public extension View {
152 | func dateTimePickerDialog(isShowing: Binding,
153 | cancelOnTapOutside: Bool = false,
154 | selection: Binding,
155 | in range: ClosedRange = .distantPast ... .distantFuture,
156 | displayComponents components: DatePickerComponents = [.hourAndMinute, .date],
157 | style: DateTimePickerDialog.Style = .default,
158 | backgroundColor: Color? = nil,
159 | foregroundColor: Color? = nil,
160 | buttons: [DateTimePickerDialog.Button] = DateTimePickerDialog.defaultButtons,
161 | label: (() -> Text)? = nil) -> some View {
162 | let dialog = DateTimePickerDialog(isShowing: isShowing,
163 | selection: selection,
164 | range: range,
165 | components: components,
166 | style: style,
167 | backgroundColor: backgroundColor,
168 | foregroundColor: foregroundColor,
169 | buttons: buttons,
170 | label: label)
171 | return self.genericDialog(isShowing: isShowing,
172 | cancelOnTapOutside: cancelOnTapOutside,
173 | cancelAction: { dialog.cancel() }) {
174 | dialog
175 | }
176 | }
177 |
178 | }
179 |
180 | struct DateTimePickerDialogTest: View {
181 | @State private var isShowing = true
182 | @State private var date = Date()
183 | let style: DateTimePickerDialog.Style
184 | let displayComponents: DatePickerComponents
185 | private let backgroundColor = Color.blue.opacity(0.1)
186 | private let textColor = Color.orange
187 |
188 | var body: some View {
189 | List(1..<6) {
190 | Text("\($0)")
191 | .onTapGesture {
192 | self.isShowing = true
193 | }
194 | }.dateTimePickerDialog(isShowing: $isShowing,
195 | cancelOnTapOutside: true,
196 | selection: $date,
197 | displayComponents: displayComponents,
198 | style: style,
199 | // backgroundColor: backgroundColor,
200 | // foregroundColor: textColor,
201 | buttons: [
202 | .default(label: "Default"),
203 | .cancel(label: "Cancel"),
204 | .custom(label: { Text("Call mom").foregroundColor(.red) }, action: { dialog in
205 |
206 | })
207 | ]) {
208 | Text("Custom label")
209 | }
210 | }
211 | }
212 |
213 | struct DateTimePickerDialog_Previews: PreviewProvider {
214 | static var previews: some View {
215 | DateTimePickerDialogTest(style: .default, displayComponents: [.date, .hourAndMinute])
216 | }
217 | }
218 |
219 | struct DateTimePickerDialogWheel_Previews: PreviewProvider {
220 | static var previews: some View {
221 | DateTimePickerDialogTest(style: .wheel, displayComponents: [.date, .hourAndMinute])
222 | }
223 | }
224 |
225 | @available(iOS 14.0, *)
226 | struct DateTimePickerDialogCompact_Previews: PreviewProvider {
227 | static var previews: some View {
228 | DateTimePickerDialogTest(style: .compact, displayComponents: [.date, .hourAndMinute])
229 | }
230 | }
231 |
232 | @available(iOS 14.0, *)
233 | struct DateTimePickerDialogGraphical_Previews: PreviewProvider {
234 | static var previews: some View {
235 | DateTimePickerDialogTest(style: .graphical, displayComponents: [.date, .hourAndMinute])
236 | }
237 | }
238 |
--------------------------------------------------------------------------------