Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
de1fae4
Initial DatePicker implementation in UIKit
bbrk24 Oct 31, 2025
2b757de
Add DatePicker for AppKitBackend
bbrk24 Nov 1, 2025
3f1bfcb
Add DatePickerExample to macOS CI
bbrk24 Nov 1, 2025
fd01725
Fix DatePicker update logic for AppKitBackend
bbrk24 Nov 1, 2025
5106638
Update argument name to match SwiftUI
bbrk24 Nov 1, 2025
1d4073d
Add more availability annotations
bbrk24 Nov 1, 2025
9823cf1
Shut up tvOS let me see if the iOS CI will pass
bbrk24 Nov 1, 2025
6f276fe
please work.
bbrk24 Nov 1, 2025
a539229
Fine, here's your view
bbrk24 Nov 1, 2025
096cd58
Initial WinUI implementation
bbrk24 Nov 2, 2025
d019996
Reformat WinUI code
bbrk24 Nov 2, 2025
890f1c1
Implement minYear/maxYear for DatePicker
bbrk24 Nov 2, 2025
2bfe6bb
Improve WinUI sizing code
bbrk24 Nov 2, 2025
03ad175
Fix CalendarDatePicker size
bbrk24 Nov 3, 2025
b05eecb
Minor cleanup
bbrk24 Nov 3, 2025
b8c5fd2
Generate GTK classes and improve manual type conversion
bbrk24 Nov 4, 2025
c454cdf
oops
bbrk24 Nov 4, 2025
34fe6a4
Fix casing of calendar name
bbrk24 Nov 4, 2025
21a0a9a
Saving partial work on GtkBackend
bbrk24 Nov 4, 2025
d763c22
More partial work
bbrk24 Nov 5, 2025
12265d2
Use Gtk.Calendar
bbrk24 Nov 6, 2025
9b59144
Add missing parts to GtkBackend.updateDatePicker
bbrk24 Nov 6, 2025
597c64e
Add DatePickerExample to Linux CI
bbrk24 Nov 6, 2025
fee1fca
Fix one Mac availability error
bbrk24 Nov 6, 2025
8d7c888
Add availability annotation on unused widget
bbrk24 Nov 6, 2025
0020599
Add time zone listener for UIKitBackend
bbrk24 Nov 7, 2025
4df7694
Add listener for AppKitBackend
bbrk24 Nov 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/build-test-and-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ jobs:
swift build --target SpreadsheetExample && \
swift build --target NotesExample && \
swift build --target GtkExample && \
swift build --target PathsExample
swift build --target PathsExample && \
swift build --target DatePickerExample

- name: Test
run: swift test --test-product swift-cross-uiPackageTests
Expand Down Expand Up @@ -106,9 +107,10 @@ jobs:
buildtarget PathsExample

if [ $device_type != TV ]; then
# Slider is not implemented for tvOS
# Slider and DatePicker are not implemented for tvOS
buildtarget ControlsExample
buildtarget RandomNumberGeneratorExample
buildtarget DatePickerExample
fi

if [ $device_type = iPad ]; then
Expand Down Expand Up @@ -165,6 +167,7 @@ jobs:
buildtarget PathsExample
buildtarget ControlsExample
buildtarget RandomNumberGeneratorExample
buildtarget DatePickerExample
# TODO test whether this works on Catalyst
# buildtarget SplitExample

Expand Down Expand Up @@ -295,7 +298,8 @@ jobs:
swift build --target StressTestExample && \
swift build --target SpreadsheetExample && \
swift build --target NotesExample && \
swift build --target GtkExample
swift build --target GtkExample && \
swift build --target DatePickerExample

- name: Test
run: swift test --test-product swift-cross-uiPackageTests
Expand Down
5 changes: 5 additions & 0 deletions Examples/Bundler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,8 @@ version = '0.1.0'
identifier = 'dev.swiftcrossui.HoverExample'
product = 'HoverExample'
version = '0.1.0'

[apps.DatePickerExample]
identifier = 'dev.swiftcrossui.DatePickerExample'
product = 'DatePickerExample'
version = '0.1.0'
12 changes: 6 additions & 6 deletions Examples/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Examples/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ let package = Package(
.executableTarget(
name: "HoverExample",
dependencies: exampleDependencies
),
.executableTarget(
name: "DatePickerExample",
dependencies: exampleDependencies
)
]
)
53 changes: 53 additions & 0 deletions Examples/Sources/DatePickerExample/DatePickerApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import DefaultBackend
import Foundation
import SwiftCrossUI

#if canImport(SwiftBundlerRuntime)
import SwiftBundlerRuntime
#endif

@main
@HotReloadable
struct DatePickerApp: App {
@State var date = Date()
@State var style: DatePickerStyle? = .automatic

var allStyles: [DatePickerStyle]

init() {
allStyles = [.automatic]

if #available(iOS 14, macCatalyst 14, *) {
allStyles.append(.graphical)
}

#if !canImport(GtkBackend)
if #available(iOS 13.4, macCatalyst 13.4, *) {
allStyles.append(.compact)
#if os(iOS) || os(visionOS) || canImport(WinUIBackend)
allStyles.append(.wheel)
#endif
}
#endif
}

var body: some Scene {
WindowGroup("Date Picker") {
VStack {
Text("Selected date: \(date)")

Picker(of: allStyles, selection: $style)

DatePicker(
"Test Picker",
selection: $date
)
.datePickerStyle(style ?? .automatic)

Button("Reset date") {
date = Date()
}
}
}
}
}
12 changes: 6 additions & 6 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ let package = Package(
),
.package(
url: "https://github.com/stackotter/swift-windowsappsdk",
revision: "ba6f0ec377b70d8be835d253102ff665a0e47d99"
revision: "f1c50892f10c0f7f635d3c7a3d728fd634ad001a"
),
.package(
url: "https://github.com/stackotter/swift-windowsfoundation",
revision: "4ad57d20553514bcb23724bdae9121569b19f172"
),
.package(
url: "https://github.com/stackotter/swift-winui",
revision: "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86"
revision: "42c47f4e4129c8b5a5d9912f05e1168c924ac180"
),
// .package(
// url: "https://github.com/stackotter/TermKit",
Expand Down
98 changes: 98 additions & 0 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,14 @@ public final class AppKitBackend: AppBackend {
// Self.scrollBarWidth has changed
action()
}

NotificationCenter.default.addObserver(
forName: .NSSystemTimeZoneDidChange,
object: nil,
queue: .main
) { _ in
action()
}
}

public func computeWindowEnvironment(
Expand Down Expand Up @@ -1689,6 +1697,80 @@ public final class AppKitBackend: AppBackend {
let request = URLRequest(url: url)
webView.load(request)
}

public func createDatePicker() -> NSView {
let datePicker = CustomDatePicker()
datePicker.delegate = datePicker.strongDelegate
return datePicker
}

// Depending on the calendar, era is either necessary or must be omitted. Making the wrong
// choice for the current calendar means the cursor position is reset after every keystroke. I
// know of no simple way to tell whether NSDatePicker requires or forbids eras for a given
// calendar, so in lieu of that I have hardcoded the calendar identifiers.
private let calendarsWithEras: Set<Calendar.Identifier> = [
.buddhist, .coptic, .ethiopicAmeteAlem, .ethiopicAmeteMihret, .indian, .islamic,
.islamicCivil, .islamicTabular, .islamicUmmAlQura, .japanese, .persian, .republicOfChina,
]

public func updateDatePicker(
_ datePicker: NSView,
environment: EnvironmentValues,
date: Date,
range: ClosedRange<Date>,
components: DatePickerComponents,
onChange: @escaping (Date) -> Void
) {
let datePicker = datePicker as! CustomDatePicker

datePicker.isEnabled = environment.isEnabled
datePicker.textColor = environment.suggestedForegroundColor.nsColor

// If the time zone is set to autoupdatingCurrent, then the cursor position is reset after
// every keystroke. Thanks Apple
datePicker.timeZone =
environment.timeZone == .autoupdatingCurrent ? .current : environment.timeZone

// A couple properties cause infinite update loops if we assign to them on every update, so
// check their values first.
if datePicker.calendar != environment.calendar {
datePicker.calendar = environment.calendar
}

if datePicker.dateValue != date {
datePicker.dateValue = date
}

var elementFlags: NSDatePicker.ElementFlags = []
if components.contains(.date) {
elementFlags.insert(.yearMonthDay)
if calendarsWithEras.contains(environment.calendar.identifier) {
elementFlags.insert(.era)
}
}
if components.contains(.hourMinuteAndSecond) {
elementFlags.insert(.hourMinuteSecond)
} else {
elementFlags.insert(.hourMinute)
}

if datePicker.datePickerElements != elementFlags {
datePicker.datePickerElements = elementFlags
}

datePicker.strongDelegate.onChange = onChange

datePicker.minDate = range.lowerBound
datePicker.maxDate = range.upperBound

datePicker.datePickerStyle =
switch environment.datePickerStyle {
case .automatic, .compact:
.textFieldAndStepper
case .graphical:
.clockAndCalendar
}
}
}

final class NSCustomTapGestureTarget: NSView {
Expand Down Expand Up @@ -2191,3 +2273,19 @@ final class CustomWKNavigationDelegate: NSObject, WKNavigationDelegate {
onNavigate?(url)
}
}

final class CustomDatePicker: NSDatePicker {
var strongDelegate = CustomDatePickerDelegate()
}

final class CustomDatePickerDelegate: NSObject, NSDatePickerCellDelegate {
var onChange: ((Date) -> Void)?

func datePickerCell(
_: NSDatePickerCell,
validateProposedDateValue proposedDateValue: AutoreleasingUnsafeMutablePointer<NSDate>,
timeInterval _: UnsafeMutablePointer<TimeInterval>?
) {
onChange?(proposedDateValue.pointee as Date)
}
}
Loading
Loading