├── .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 | ![Graphical dialog](https://github.com/globulus/swiftui-date-picker-dialog/blob/main/Images/graphical-green-pink.png?raw=true) 20 | 21 | * Compact-style (default): 22 | 23 | ![Compact dialog](https://github.com/globulus/swiftui-date-picker-dialog/blob/main/Images/compact-red-purple.png?raw=true) 24 | 25 | * Wheel dialog: 26 | 27 | ![Wheel dialog](https://github.com/globulus/swiftui-date-picker-dialog/blob/main/Images/wheel-orange-blue.png?raw=true) 28 | 29 | * Dialog with custom buttons: 30 | 31 | ![Custom button dialog](https://github.com/globulus/swiftui-date-picker-dialog/blob/main/Images/custom-button.png?raw=true) 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 | --------------------------------------------------------------------------------