Skip to content

Commit abac684

Browse files
committed
Initial DatePicker implementation in UIKit
Add DatePicker for AppKitBackend Add DatePickerExample to macOS CI Fix DatePicker update logic for AppKitBackend Update argument name to match SwiftUI Add more availability annotations Shut up tvOS let me see if the iOS CI will pass please work. Fine, here's your view Initial WinUI implementation Reformat WinUI code Implement minYear/maxYear for DatePicker Improve WinUI sizing code Fix CalendarDatePicker size Minor cleanup Generate GTK classes and improve manual type conversion oops Fix casing of calendar name Saving partial work on GtkBackend More partial work Use Gtk.Calendar Add missing parts to GtkBackend.updateDatePicker Add DatePickerExample to Linux CI Fix one Mac availability error Add availability annotation on unused widget Add time zone listener for UIKitBackend Add listener for AppKitBackend
1 parent 7406f22 commit abac684

28 files changed

+2608
-52
lines changed

.github/workflows/build-test-and-docs.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ jobs:
5252
swift build --target SpreadsheetExample && \
5353
swift build --target NotesExample && \
5454
swift build --target GtkExample && \
55-
swift build --target PathsExample
55+
swift build --target PathsExample && \
56+
swift build --target DatePickerExample
5657
5758
- name: Test
5859
run: swift test --test-product swift-cross-uiPackageTests
@@ -106,9 +107,10 @@ jobs:
106107
buildtarget PathsExample
107108
108109
if [ $device_type != TV ]; then
109-
# Slider is not implemented for tvOS
110+
# Slider and DatePicker are not implemented for tvOS
110111
buildtarget ControlsExample
111112
buildtarget RandomNumberGeneratorExample
113+
buildtarget DatePickerExample
112114
fi
113115
114116
if [ $device_type = iPad ]; then
@@ -165,6 +167,7 @@ jobs:
165167
buildtarget PathsExample
166168
buildtarget ControlsExample
167169
buildtarget RandomNumberGeneratorExample
170+
buildtarget DatePickerExample
168171
# TODO test whether this works on Catalyst
169172
# buildtarget SplitExample
170173
@@ -305,7 +308,8 @@ jobs:
305308
swift build --target StressTestExample && \
306309
swift build --target SpreadsheetExample && \
307310
swift build --target NotesExample && \
308-
swift build --target GtkExample
311+
swift build --target GtkExample && \
312+
swift build --target DatePickerExample
309313
310314
- name: Test
311315
run: swift test --test-product swift-cross-uiPackageTests

Examples/Bundler.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,8 @@ version = '0.1.0'
6464
identifier = 'dev.swiftcrossui.HoverExample'
6565
product = 'HoverExample'
6666
version = '0.1.0'
67+
68+
[apps.DatePickerExample]
69+
identifier = 'dev.swiftcrossui.DatePickerExample'
70+
product = 'DatePickerExample'
71+
version = '0.1.0'

Examples/Package.resolved

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Examples/Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ let package = Package(
7676
.executableTarget(
7777
name: "HoverExample",
7878
dependencies: exampleDependencies
79+
),
80+
.executableTarget(
81+
name: "DatePickerExample",
82+
dependencies: exampleDependencies
7983
)
8084
]
8185
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import DefaultBackend
2+
import Foundation
3+
import SwiftCrossUI
4+
5+
#if canImport(SwiftBundlerRuntime)
6+
import SwiftBundlerRuntime
7+
#endif
8+
9+
@main
10+
@HotReloadable
11+
struct DatePickerApp: App {
12+
@State var date = Date()
13+
@State var style: DatePickerStyle? = .automatic
14+
15+
var allStyles: [DatePickerStyle]
16+
17+
init() {
18+
allStyles = [.automatic]
19+
20+
if #available(iOS 14, macCatalyst 14, *) {
21+
allStyles.append(.graphical)
22+
}
23+
24+
#if !canImport(GtkBackend)
25+
if #available(iOS 13.4, macCatalyst 13.4, *) {
26+
allStyles.append(.compact)
27+
#if os(iOS) || os(visionOS) || canImport(WinUIBackend)
28+
allStyles.append(.wheel)
29+
#endif
30+
}
31+
#endif
32+
}
33+
34+
var body: some Scene {
35+
WindowGroup("Date Picker") {
36+
VStack {
37+
Text("Selected date: \(date)")
38+
39+
Picker(of: allStyles, selection: $style)
40+
41+
DatePicker(
42+
"Test Picker",
43+
selection: $date
44+
)
45+
.datePickerStyle(style ?? .automatic)
46+
47+
Button("Reset date") {
48+
date = Date()
49+
}
50+
}
51+
}
52+
}
53+
}

Package.resolved

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,15 @@ let package = Package(
100100
),
101101
.package(
102102
url: "https://github.com/stackotter/swift-windowsappsdk",
103-
revision: "ba6f0ec377b70d8be835d253102ff665a0e47d99"
103+
revision: "f1c50892f10c0f7f635d3c7a3d728fd634ad001a"
104104
),
105105
.package(
106106
url: "https://github.com/stackotter/swift-windowsfoundation",
107107
revision: "4ad57d20553514bcb23724bdae9121569b19f172"
108108
),
109109
.package(
110110
url: "https://github.com/stackotter/swift-winui",
111-
revision: "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86"
111+
revision: "42c47f4e4129c8b5a5d9912f05e1168c924ac180"
112112
),
113113
// .package(
114114
// url: "https://github.com/stackotter/TermKit",

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,14 @@ public final class AppKitBackend: AppBackend {
350350
// Self.scrollBarWidth has changed
351351
action()
352352
}
353+
354+
NotificationCenter.default.addObserver(
355+
forName: .NSSystemTimeZoneDidChange,
356+
object: nil,
357+
queue: .main
358+
) { _ in
359+
action()
360+
}
353361
}
354362

355363
public func computeWindowEnvironment(
@@ -1803,6 +1811,80 @@ public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate {
18031811
onDismiss?()
18041812
}
18051813
}
1814+
1815+
public func createDatePicker() -> NSView {
1816+
let datePicker = CustomDatePicker()
1817+
datePicker.delegate = datePicker.strongDelegate
1818+
return datePicker
1819+
}
1820+
1821+
// Depending on the calendar, era is either necessary or must be omitted. Making the wrong
1822+
// choice for the current calendar means the cursor position is reset after every keystroke. I
1823+
// know of no simple way to tell whether NSDatePicker requires or forbids eras for a given
1824+
// calendar, so in lieu of that I have hardcoded the calendar identifiers.
1825+
private let calendarsWithEras: Set<Calendar.Identifier> = [
1826+
.buddhist, .coptic, .ethiopicAmeteAlem, .ethiopicAmeteMihret, .indian, .islamic,
1827+
.islamicCivil, .islamicTabular, .islamicUmmAlQura, .japanese, .persian, .republicOfChina,
1828+
]
1829+
1830+
public func updateDatePicker(
1831+
_ datePicker: NSView,
1832+
environment: EnvironmentValues,
1833+
date: Date,
1834+
range: ClosedRange<Date>,
1835+
components: DatePickerComponents,
1836+
onChange: @escaping (Date) -> Void
1837+
) {
1838+
let datePicker = datePicker as! CustomDatePicker
1839+
1840+
datePicker.isEnabled = environment.isEnabled
1841+
datePicker.textColor = environment.suggestedForegroundColor.nsColor
1842+
1843+
// If the time zone is set to autoupdatingCurrent, then the cursor position is reset after
1844+
// every keystroke. Thanks Apple
1845+
datePicker.timeZone =
1846+
environment.timeZone == .autoupdatingCurrent ? .current : environment.timeZone
1847+
1848+
// A couple properties cause infinite update loops if we assign to them on every update, so
1849+
// check their values first.
1850+
if datePicker.calendar != environment.calendar {
1851+
datePicker.calendar = environment.calendar
1852+
}
1853+
1854+
if datePicker.dateValue != date {
1855+
datePicker.dateValue = date
1856+
}
1857+
1858+
var elementFlags: NSDatePicker.ElementFlags = []
1859+
if components.contains(.date) {
1860+
elementFlags.insert(.yearMonthDay)
1861+
if calendarsWithEras.contains(environment.calendar.identifier) {
1862+
elementFlags.insert(.era)
1863+
}
1864+
}
1865+
if components.contains(.hourMinuteAndSecond) {
1866+
elementFlags.insert(.hourMinuteSecond)
1867+
} else {
1868+
elementFlags.insert(.hourMinute)
1869+
}
1870+
1871+
if datePicker.datePickerElements != elementFlags {
1872+
datePicker.datePickerElements = elementFlags
1873+
}
1874+
1875+
datePicker.strongDelegate.onChange = onChange
1876+
1877+
datePicker.minDate = range.lowerBound
1878+
datePicker.maxDate = range.upperBound
1879+
1880+
datePicker.datePickerStyle =
1881+
switch environment.datePickerStyle {
1882+
case .automatic, .compact:
1883+
.textFieldAndStepper
1884+
case .graphical:
1885+
.clockAndCalendar
1886+
}
1887+
}
18061888
}
18071889

18081890
final class NSCustomTapGestureTarget: NSView {
@@ -2310,3 +2392,19 @@ final class CustomWKNavigationDelegate: NSObject, WKNavigationDelegate {
23102392
onNavigate?(url)
23112393
}
23122394
}
2395+
2396+
final class CustomDatePicker: NSDatePicker {
2397+
var strongDelegate = CustomDatePickerDelegate()
2398+
}
2399+
2400+
final class CustomDatePickerDelegate: NSObject, NSDatePickerCellDelegate {
2401+
var onChange: ((Date) -> Void)?
2402+
2403+
func datePickerCell(
2404+
_: NSDatePickerCell,
2405+
validateProposedDateValue proposedDateValue: AutoreleasingUnsafeMutablePointer<NSDate>,
2406+
timeInterval _: UnsafeMutablePointer<TimeInterval>?
2407+
) {
2408+
onChange?(proposedDateValue.pointee as Date)
2409+
}
2410+
}

0 commit comments

Comments
 (0)