From eac015b00d6d828048379f766d42b4d6e5a9f1b9 Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Wed, 22 Apr 2026 16:34:42 -0400 Subject: [PATCH 01/12] Migrate to Swift 6 with strict concurrency, bump to v5.0.0 - Enable Swift 6 language mode and StrictConcurrency in Package.swift - Update SWIFT_VERSION to 6.0 in Xcode project - Add Sendable conformances (Order, HeartCompatible) - Add @MainActor annotations for UI-bound Rx extensions - Fix MainActor function reference conversion in ManualDateField - Remove CocoaPods podspec and Carthage files - Update CI workflow to Xcode 16.4 on macos-15 - Bump SPM version from 4.0.0 to 5.0.0 in README --- .github/workflows/ci.yml | 17 ++++++ Cartfile | 1 - Cartfile.private | 3 -- Cartfile.resolved | 3 -- Cordate.podspec | 26 --------- Cordate.xcodeproj/project.pbxproj | 8 +-- .../Core/Calendar/CalendarDataSource.swift | 26 ++++----- .../CalendarDateSelectionController.swift | 51 +----------------- .../Core/Calendar/CalendarLayout.swift | 2 +- .../Sources/Core/Calendar/CalendarStyle.swift | 53 ++----------------- .../CalendarPresentationController.swift | 15 +----- .../ManualDateField+Format.swift | 9 ++-- .../ManualDateField+Order.swift | 2 +- .../ManualDateField+Separator.swift | 2 +- .../ManualDateField/ManualDateField.swift | 2 +- Cordate/Sources/Core/Extensions/Heart.swift | 6 +-- .../Core/Extensions/Internal/UIButton.swift | 8 +-- .../Internal/UIViewController.swift | 24 ++------- Cordate/Sources/Rx/Rx+Calendar.swift | 11 ++-- Cordate/Sources/Rx/Rx+ManualDateField.swift | 6 +-- Package.resolved | 25 +++++---- Package.swift | 11 ++-- README.md | 16 +++--- 23 files changed, 92 insertions(+), 235 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 Cartfile delete mode 100644 Cartfile.private delete mode 100644 Cartfile.resolved delete mode 100644 Cordate.podspec diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3fb6f6b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + build: + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer + - name: Build + run: xcodebuild build -scheme Cordate -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' diff --git a/Cartfile b/Cartfile deleted file mode 100644 index 6408fb7..0000000 --- a/Cartfile +++ /dev/null @@ -1 +0,0 @@ -github "ReactiveX/RxSwift" \ No newline at end of file diff --git a/Cartfile.private b/Cartfile.private deleted file mode 100644 index 9c39383..0000000 --- a/Cartfile.private +++ /dev/null @@ -1,3 +0,0 @@ -github "Quick/Quick" -github "Quick/Nimble" - diff --git a/Cartfile.resolved b/Cartfile.resolved deleted file mode 100644 index c36588a..0000000 --- a/Cartfile.resolved +++ /dev/null @@ -1,3 +0,0 @@ -github "Quick/Nimble" "v9.2.1" -github "Quick/Quick" "v4.0.0" -github "ReactiveX/RxSwift" "6.5.0" diff --git a/Cordate.podspec b/Cordate.podspec deleted file mode 100644 index 776b692..0000000 --- a/Cordate.podspec +++ /dev/null @@ -1,26 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'Cordate' - s.version = '3.0.3' - s.license = 'MIT' - s.summary = 'Give dates a special place in your ❤️' - s.description = 'Cordate is a small library which makes working with dates much smoother by adding commonly-used extensions, custom UI components, and more.' - s.author = 'Duet Health' - s.source = { git: 'https://github.com/DuetHealth/Cordate.git', tag: s.version } - s.homepage = s.source[:git] - s.ios.deployment_target = '12.0' - s.requires_arc = true - s.default_subspec = 'Core' - s.swift_version = '5.5' - - s.subspec 'Core' do |core| - core.source_files = 'Cordate/Sources/Core/**/*.{h,m,swift}' - end - - s.subspec 'Rx' do |rx| - rx.source_files = 'Cordate/Sources/**/*.{h,m,swift}' - - rx.dependency 'RxCocoa' - rx.dependency 'RxSwift' - end - -end diff --git a/Cordate.xcodeproj/project.pbxproj b/Cordate.xcodeproj/project.pbxproj index f3a7a2c..c2c36e4 100644 --- a/Cordate.xcodeproj/project.pbxproj +++ b/Cordate.xcodeproj/project.pbxproj @@ -579,7 +579,7 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -609,7 +609,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -632,7 +632,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.cordate.demo; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -655,7 +655,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.cordate.demo; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/Cordate/Sources/Core/Calendar/CalendarDataSource.swift b/Cordate/Sources/Core/Calendar/CalendarDataSource.swift index 3a121f5..a8924cb 100644 --- a/Cordate/Sources/Core/Calendar/CalendarDataSource.swift +++ b/Cordate/Sources/Core/Calendar/CalendarDataSource.swift @@ -3,20 +3,11 @@ import UIKit public class CalendarDataSource { - public enum CalendarMode { + public enum CalendarMode: Sendable { case birthDate - - var range: (minimum: Date?, maximum: Date?) { - switch self { - case .birthDate: - let maximum = Date().heart.local - let minimum = Date(day: 1, month: 1, year: maximum.heart.year - 150) - return (minimum, maximum) - } - } } - public struct Common { + public struct Common: Sendable { private static let formatter: DateFormatter = { let formatter = DateFormatter() @@ -34,7 +25,7 @@ public class CalendarDataSource { } - public enum Component { + public enum Component: Sendable { case day(month: Int, year: Int) case month(year: Int) case year @@ -47,13 +38,22 @@ public class CalendarDataSource { return minimumDate.heart.year...maximumDate.heart.year } + private static func modeRange(_ mode: CalendarMode) -> (minimum: Date?, maximum: Date?) { + switch mode { + case .birthDate: + let maximum = Date().heart.local + let minimum = Date(day: 1, month: 1, year: maximum.heart.year - 150) + return (minimum, maximum) + } + } + public init(minimumDate: Date? = nil, maximumDate: Date? = nil) { self.minimumDate = minimumDate ?? Date.distantPast self.maximumDate = maximumDate ?? Date.distantFuture } public convenience init(mode: CalendarMode) { - let (minimum, maximum) = mode.range + let (minimum, maximum) = CalendarDataSource.modeRange(mode) self.init(minimumDate: minimum, maximumDate: maximum) } diff --git a/Cordate/Sources/Core/Calendar/CalendarDateSelectionController.swift b/Cordate/Sources/Core/Calendar/CalendarDateSelectionController.swift index 42d4b7e..8d14b17 100644 --- a/Cordate/Sources/Core/Calendar/CalendarDateSelectionController.swift +++ b/Cordate/Sources/Core/Calendar/CalendarDateSelectionController.swift @@ -1,16 +1,9 @@ import Foundation import UIKit -/// Adopters of the 'CalendarDateSelectionControllerDelegate` respond to events triggered by user -/// interaction with the calendar. @objc public protocol CalendarDateSelectionControllerDelegate: UICollectionViewDelegate { - - /// Called by the calendar when a user cleared the date. @objc optional func calendarControllerClearedDate(_ controller: CalendarDateSelectionController) - - /// Called by calendar when a user selects a date. @objc optional func calendarController(_ controller: CalendarDateSelectionController, didSelectDate date: Date) - } public class CalendarDateSelectionController: UIViewController { @@ -19,16 +12,9 @@ public class CalendarDateSelectionController: UIViewController { private static let reuseIdentifier = "com.Cordate.calendar" - /// Returns the title shown at the top of the calendar. public let calendarTitle: String? - - /// Returns the data source driving the calendar. public let dataSource: CalendarDataSource - - /// Returns the view which renders the calendar components. public let calendarView: UICollectionView - - /// Returns the layout object which drives the calendar layout. public let calendarLayout = CalendarLayout() private let titleLabel = UILabel() @@ -38,12 +24,10 @@ public class CalendarDateSelectionController: UIViewController { private let clearButton = UIButton(type: .system) private let confirmButton = UIButton(type: .system) - /// The currently-selected date. @objc dynamic public var date = Date?.none { didSet { confirmButton.isEnabled = date != nil } } - /// The style collection for the calendar. public var style = CalendarStyle() { didSet { applyStyle() } } @@ -52,7 +36,6 @@ public class CalendarDateSelectionController: UIViewController { return CalendarPresentationController(presentedViewController: self, presenting: nil) }() - /// The delegate which receives events from the calendar. public weak var delegate = CalendarDateSelectionControllerDelegate?.none private var currentComponent = CalendarDataSource.Component.year { @@ -78,11 +61,9 @@ public class CalendarDateSelectionController: UIViewController { private var selectedMonth = Int?.none { didSet { selectedDay = nil } } - + private var selectedDay = Int?.none - /// Performs an initial layout pass in viewDidLayoutSubviews which prepares the collection view's - /// layout and, if applicable, scrolls to the selected year. private var hasLaidOutOnce = false public init(dataSource: CalendarDataSource = CalendarDataSource(), title: String? = nil, initialDate: Date? = nil) { @@ -290,8 +271,6 @@ public class CalendarDateSelectionController: UIViewController { } private func triggerReload() { - // TODO : Determine how to set the content offset or scroll to an item as part of an animated - // reload. The content offset/item to scroll to needs set up prior to animating the cells. UIView.animate(withDuration: 0.2, animations: { self.calendarView.alpha = 0 self.calendarHeader.alpha = 0 @@ -399,7 +378,7 @@ extension CalendarDateSelectionController: UICollectionViewDelegate { } public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let generator = SelectionFeedbackGenerator() + let generator = UISelectionFeedbackGenerator() generator.prepare() switch currentComponent { case .year: @@ -412,8 +391,6 @@ extension CalendarDateSelectionController: UICollectionViewDelegate { currentComponent = .month(year: selectedYear!) generator.selectionChanged() guard let selectedCell: ComponentCell = collectionView.cell(for: indexPath) else { - // This and the other similar cases should never happen for obvious reasons, but in - // the event that it does this will ensure that usability isn't totally hampered. triggerReload() return } @@ -497,27 +474,3 @@ extension CalendarDateSelectionController: UIViewControllerTransitioningDelegate fileprivate func warn(_ message: String, _ function: String = #function, _ line: Int = #line) { print("\(CalendarDateSelectionController.self).\(function):\(line) WARN - \(message).") } - -fileprivate struct SelectionFeedbackGenerator { - - private let generator: Any? - - init() { - if #available(iOS 10.0, *) { - generator = UISelectionFeedbackGenerator() - } else { generator = nil } - } - - func prepare() { - if #available(iOS 10.0, *) { - (generator as? UISelectionFeedbackGenerator)?.prepare() - } - } - - func selectionChanged() { - if #available(iOS 10.0, *) { - (generator as? UISelectionFeedbackGenerator)?.selectionChanged() - } - } - -} diff --git a/Cordate/Sources/Core/Calendar/CalendarLayout.swift b/Cordate/Sources/Core/Calendar/CalendarLayout.swift index 2eec08d..160fb34 100644 --- a/Cordate/Sources/Core/Calendar/CalendarLayout.swift +++ b/Cordate/Sources/Core/Calendar/CalendarLayout.swift @@ -3,7 +3,7 @@ import UIKit public class CalendarLayout: UICollectionViewFlowLayout { - public enum AnimationStyle { + public enum AnimationStyle: Sendable { case simple case detailed } diff --git a/Cordate/Sources/Core/Calendar/CalendarStyle.swift b/Cordate/Sources/Core/Calendar/CalendarStyle.swift index 283bcc6..2438bde 100644 --- a/Cordate/Sources/Core/Calendar/CalendarStyle.swift +++ b/Cordate/Sources/Core/Calendar/CalendarStyle.swift @@ -1,7 +1,7 @@ import Foundation import UIKit -public struct CalendarStyle { +public struct CalendarStyle: Sendable { static func textColor(for color: UIColor) -> UIColor? { guard color != .clear else { return .black } @@ -19,71 +19,24 @@ public struct CalendarStyle { return .white } - /// Drives which buttons are shown on the calendar. - public enum ButtonConfiguration { - - /// The calendar shows no buttons and produces values continuously. + public enum ButtonConfiguration: Sendable { case none - - /// The calendar shows a confirmation button and only produces a value when it is pressed. case confirmationOnly - - /// The calendar shows both a confirmation button and a clear button. case all - } - /// The background color for unselected date components. public var unselectedBackgroundColor = UIColor(white: 0.95, alpha: 1) - - /// The text color for unselected date components. - /// - /// When this value is `nil`, the calendar will choose either white or black text depending on - /// the intensity of the current background color. public var unselectedTextColor = UIColor?.none - - /// The text color for selected date components. - /// - /// When this value is `nil`, the calendar will choose either white or black text depending on - /// the intensity of the current background color. public var selectedTextColor = UIColor?.none - - /// The background color for selected date components. - /// - /// When this value is `nil`, the calendar will use its view's tint color. public var selectedBackgroundColor = UIColor?.none - - /// The text color for the day-mode header. public var headerTextColor = UIColor.black - - /// The tint color for the calendar mode buttons in the enabled state. - /// - /// When this value is `nil`, the calendar will use its view's tint color. public var enabledCalendarModeButtonColor = UIColor?.none - - /// The tint color for the calendar mode buttons in the disabled state. public var disabledCalendarModeButtonColor = UIColor(white: 0.9, alpha: 1) - - /// The font for the calendar mode buttons. public var calendarModeButtonFont = UIFont.boldSystemFont(ofSize: 16) - public var titleFont = UIFont.boldSystemFont(ofSize: 18) - public var calendarHeaderFont = UIFont.boldSystemFont(ofSize: 14) - - /// The font for the calendar controls. public var calendarFont = UIFont.systemFont(ofSize: UIFont.systemFontSize) - - private var _usesHaptics: Bool = true - - /// Controls whether selection events trigger haptic feedback. - @available(iOS 10.0, *) - public var usesHaptics: Bool { - get { return _usesHaptics } - set { _usesHaptics = newValue } - } - - /// Controls which controls are provided by the calendar. + public var usesHaptics: Bool = true public var buttonConfiguration = ButtonConfiguration.all public init() { } diff --git a/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift b/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift index c6b7df2..d478c97 100644 --- a/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift +++ b/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift @@ -1,25 +1,13 @@ import Foundation import UIKit -fileprivate let defaultStyle: UIBlurEffect.Style = { - if #available(iOS 10.0, *) { return .regular } - return .light -}() - -/// The `CalendarPresentationController` class is a subclass of `UIPresentationController` which -/// provides default behaviors and styling for the `CalendarDateSelectionController`. public class CalendarPresentationController: UIPresentationController { private let blurEffectView = UIVisualEffectView() private let tapGestureRecognizer = UITapGestureRecognizer() - /// The blur effect used by the overlay. - /// On iOS 10 and above, this defaults to `.regular`; on iOS 9, the default is `.light`. - public var blurEffect = UIBlurEffect(style: defaultStyle) + public var blurEffect = UIBlurEffect(style: .regular) - /// Controls whether touches on the blur overlay trigger a dismissal. - /// - /// The default value of this is `true`. public var overlayTouchesShouldDismiss: Bool = true { didSet { tapGestureRecognizer.isEnabled = overlayTouchesShouldDismiss @@ -73,4 +61,3 @@ public class CalendarPresentationController: UIPresentationController { } } - diff --git a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Format.swift b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Format.swift index e2f328c..a339f97 100644 --- a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Format.swift +++ b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Format.swift @@ -2,17 +2,17 @@ import Foundation public extension ManualDateField { - struct Format { + struct Format: Sendable { - enum Day: String { + enum Day: String, Sendable { case `default` = "dd" } - enum Month: String { + enum Month: String, Sendable { case `default` = "MM" } - public enum Year: String { + public enum Year: String, Sendable { case short = "yy" case long = "yyyy" } @@ -41,4 +41,3 @@ public extension ManualDateField { } } - diff --git a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Order.swift b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Order.swift index 898eaf2..3979fc8 100644 --- a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Order.swift +++ b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Order.swift @@ -2,7 +2,7 @@ import Foundation public extension ManualDateField { - struct Order { + struct Order: Sendable { public static let dayMonthYear = Order((.day, .month, .year)) public static let monthDayYear = Order((.month, .day, .year)) public static let yearDayMonth = Order((.year, .day, .month)) diff --git a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Separator.swift b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Separator.swift index f0d062a..2384506 100644 --- a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Separator.swift +++ b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField+Separator.swift @@ -2,7 +2,7 @@ import Foundation public extension ManualDateField { - enum Separator: String { + enum Separator: String, Sendable { case slash = "/" case dash = "-" case period = "." diff --git a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift index 086e554..c147bc6 100644 --- a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift +++ b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift @@ -107,7 +107,7 @@ public class ManualDateField: UITextField { } public override func copy() -> Any { - return text.map(formatText) ?? "" + return text.map { formatText($0) } ?? "" } public override func drawText(in rect: CGRect) { diff --git a/Cordate/Sources/Core/Extensions/Heart.swift b/Cordate/Sources/Core/Extensions/Heart.swift index e271b79..b3cb293 100644 --- a/Cordate/Sources/Core/Extensions/Heart.swift +++ b/Cordate/Sources/Core/Extensions/Heart.swift @@ -1,6 +1,6 @@ import Foundation -public struct Heart { +public struct Heart: Sendable where Base: Sendable { public let base: Base @@ -11,13 +11,13 @@ public struct Heart { } public protocol HeartCompatible { - associatedtype CompatibleType + associatedtype CompatibleType: Sendable var heart: Heart { get } } -extension HeartCompatible { +extension HeartCompatible where Self: Sendable { public var heart: Heart { return Heart(self) diff --git a/Cordate/Sources/Core/Extensions/Internal/UIButton.swift b/Cordate/Sources/Core/Extensions/Internal/UIButton.swift index ff1597d..20b2c55 100644 --- a/Cordate/Sources/Core/Extensions/Internal/UIButton.swift +++ b/Cordate/Sources/Core/Extensions/Internal/UIButton.swift @@ -1,14 +1,14 @@ import Foundation import UIKit -fileprivate var key = UInt8.max +nonisolated(unsafe) fileprivate var key = UInt8.max extension UIButton { class Closure: NSObject { - let closure: () -> () + let closure: @MainActor () -> () - init(_ closure: @escaping () -> ()) { + init(_ closure: @escaping @MainActor () -> ()) { self.closure = closure } } @@ -18,7 +18,7 @@ extension UIButton { closure.closure() } - func onTap(_ closure: @escaping () -> ()) { + func onTap(_ closure: @escaping @MainActor () -> ()) { objc_setAssociatedObject(self, &key, Closure(closure), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) addTarget(self, action: #selector(invoke), for: .touchUpInside) } diff --git a/Cordate/Sources/Core/Extensions/Internal/UIViewController.swift b/Cordate/Sources/Core/Extensions/Internal/UIViewController.swift index 2b5272e..773e0b0 100644 --- a/Cordate/Sources/Core/Extensions/Internal/UIViewController.swift +++ b/Cordate/Sources/Core/Extensions/Internal/UIViewController.swift @@ -4,35 +4,19 @@ import UIKit extension UIViewController { var topAnchor: NSLayoutYAxisAnchor { - if #available(iOS 11.0, *) { - return view.safeAreaLayoutGuide.topAnchor - } else { - return topLayoutGuide.bottomAnchor - } + return view.safeAreaLayoutGuide.topAnchor } var leftAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return view.safeAreaLayoutGuide.leftAnchor - } else { - return view.leftAnchor - } + return view.safeAreaLayoutGuide.leftAnchor } var bottomAnchor: NSLayoutYAxisAnchor { - if #available(iOS 11.0, *) { - return view.safeAreaLayoutGuide.bottomAnchor - } else { - return view.bottomAnchor - } + return view.safeAreaLayoutGuide.bottomAnchor } var rightAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return view.safeAreaLayoutGuide.rightAnchor - } else { - return view.rightAnchor - } + return view.safeAreaLayoutGuide.rightAnchor } } diff --git a/Cordate/Sources/Rx/Rx+Calendar.swift b/Cordate/Sources/Rx/Rx+Calendar.swift index cff3525..1eed825 100644 --- a/Cordate/Sources/Rx/Rx+Calendar.swift +++ b/Cordate/Sources/Rx/Rx+Calendar.swift @@ -1,12 +1,12 @@ import Foundation -import RxCocoa -import RxSwift +@preconcurrency import RxCocoa +@preconcurrency import RxSwift -fileprivate var delegateProxyKey = UInt8.max +nonisolated(unsafe) fileprivate var delegateProxyKey = UInt8.max +@MainActor public extension Reactive where Base: CalendarDateSelectionController { - /// Returns a sequence which emits changes to the selected date. var date: ControlEvent { let source = observe(Date?.self, #keyPath(CalendarDateSelectionController.date)) .filter { $0 != nil } @@ -14,7 +14,6 @@ public extension Reactive where Base: CalendarDateSelectionController { return ControlEvent(events: source) } - /// Returns a sequence which emits explicit confirmations of the selected date. var selectedDate: ControlEvent { let proxy = base.delegate as? CalendarDelegateProxy ?? installProxy() let source = proxy.rx.methodInvoked(#selector(CalendarDateSelectionControllerDelegate.calendarController(_:didSelectDate:))) @@ -22,7 +21,6 @@ public extension Reactive where Base: CalendarDateSelectionController { return ControlEvent(events: source) } - /// Returns a sequence which emits explicit removals of the selected date. var clearedDate: ControlEvent { let proxy = base.delegate as? CalendarDelegateProxy ?? installProxy() let source = proxy.rx.methodInvoked(#selector(CalendarDateSelectionControllerDelegate.calendarControllerClearedDate(_:))) @@ -39,6 +37,7 @@ public extension Reactive where Base: CalendarDateSelectionController { } +@MainActor fileprivate class CalendarDelegateProxy: NSObject, CalendarDateSelectionControllerDelegate { weak var forwardedDelegate: CalendarDateSelectionControllerDelegate? diff --git a/Cordate/Sources/Rx/Rx+ManualDateField.swift b/Cordate/Sources/Rx/Rx+ManualDateField.swift index 7afc1af..ab4c472 100644 --- a/Cordate/Sources/Rx/Rx+ManualDateField.swift +++ b/Cordate/Sources/Rx/Rx+ManualDateField.swift @@ -1,11 +1,9 @@ import Foundation -import RxCocoa -import RxSwift +@preconcurrency import RxCocoa +@preconcurrency import RxSwift public extension Reactive where Base: ManualDateField { - /// Returns a Reactive control property which emits changes to the field's date and accepts new - /// dates. var date: ControlProperty { let source = base.rx.text.map { _ in self.base.date } let sink = Binder(base) { field, date in diff --git a/Package.resolved b/Package.resolved index a785239..7b03fe5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,16 +1,15 @@ { - "object": { - "pins": [ - { - "package": "RxSwift", - "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", - "state": { - "branch": null, - "revision": "b4307ba0b6425c0ba4178e138799946c3da594f8", - "version": "6.5.0" - } + "originHash" : "fdd55bcafe2d5972feb5c769038b0b53971f4508152cbc7eb342010165d417b5", + "pins" : [ + { + "identity" : "rxswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactiveX/RxSwift.git", + "state" : { + "revision" : "132aea4f236ccadc51590b38af0357a331d51fa2", + "version" : "6.10.2" } - ] - }, - "version": 1 + } + ], + "version" : 3 } diff --git a/Package.swift b/Package.swift index b6af892..4cad3ea 100644 --- a/Package.swift +++ b/Package.swift @@ -1,22 +1,25 @@ -// swift-tools-version:5.1 +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "Cordate", - platforms: [.iOS(.v12)], + platforms: [.iOS(.v14)], products: [ .library( name: "Cordate", targets: ["Cordate"]), ], dependencies: [ - .package(url: "https://github.com/ReactiveX/RxSwift.git", .exact("6.5.0")) + .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.5.0") ], targets: [ .target( name: "Cordate", dependencies: ["RxSwift", "RxCocoa"], - path: "Cordate/Sources") + path: "Cordate/Sources", + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency") + ]) ] ) diff --git a/README.md b/README.md index 1678292..8d994e4 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,23 @@ # Cordate +[![CI](https://github.com/DuetHealth/Cordate/actions/workflows/ci.yml/badge.svg)](https://github.com/DuetHealth/Cordate/actions/workflows/ci.yml) + Cordate is a small library which makes working with dates much smoother by adding commonly-used extensions, custom UI components, and more. ## Usage ### Installation -Swift Package Manager: -``` -// swift-tools-version:5.0 +Swift Package Manager: +```swift +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "CordateTestProject", dependencies: [ - .package(url: "https://github.com/DuetHealth/Cordate.git", from: "3.0.3") + .package(url: "https://github.com/DuetHealth/Cordate.git", from: "5.0.0") ], targets: [ .target(name: "CordateTestProject", dependencies: ["Cordate"]) @@ -23,10 +25,6 @@ let package = Package( ) ``` -Cocoapods: `pod 'Cordate', '~> 3.0'`. See [Cordate.podspec](Cordate.podspec) for more information. - -Carthage: `github "DuetHealth/Cordate" ~> 3.0 && carthage update` - ## Roadmap * Refactor calendar into a `CalendarView` class to enable greater reusability @@ -35,4 +33,4 @@ Carthage: `github "DuetHealth/Cordate" ~> 3.0 && carthage update` ## License -Bedrock is MIT-licensed. The [MIT license](LICENSE) is included in the root of the repository. +Cordate is MIT-licensed. The [MIT license](LICENSE) is included in the root of the repository. From 8da9ca29f0674b613dbbf3fb961e3a57a0f124ad Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Wed, 22 Apr 2026 17:29:41 -0400 Subject: [PATCH 02/12] Fix MainActor isolation for copy() in ManualDateField --- .../Sources/Core/Controls/ManualDateField/ManualDateField.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift index c147bc6..cc0f1bd 100644 --- a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift +++ b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift @@ -106,7 +106,7 @@ public class ManualDateField: UITextField { return .zero } - public override func copy() -> Any { + @MainActor public override func copy() -> Any { return text.map { formatText($0) } ?? "" } From bc9bf6b7683210798d3300cc84bbbb59e7621948 Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Wed, 22 Apr 2026 17:32:45 -0400 Subject: [PATCH 03/12] Use MainActor.assumeIsolated for copy() to match nonisolated override --- .../Core/Controls/ManualDateField/ManualDateField.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift index cc0f1bd..9437309 100644 --- a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift +++ b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift @@ -106,10 +106,13 @@ public class ManualDateField: UITextField { return .zero } - @MainActor public override func copy() -> Any { - return text.map { formatText($0) } ?? "" + public override func copy() -> Any { + MainActor.assumeIsolated { text.map { formatText($0) } ?? "" } } + + + public override func drawText(in rect: CGRect) { let drawingRepresentation = generateDrawingRepresentation() let heightInset = (rect.size.height - drawingRepresentation.size().height) / 2 From 571d94cc3082ab5ec7a65b10824a1fab4cc4b42e Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Wed, 22 Apr 2026 17:34:47 -0400 Subject: [PATCH 04/12] Align Xcode project deployment target to iOS 14 matching Package.swift --- Cordate.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cordate.xcodeproj/project.pbxproj b/Cordate.xcodeproj/project.pbxproj index c2c36e4..243cc25 100644 --- a/Cordate.xcodeproj/project.pbxproj +++ b/Cordate.xcodeproj/project.pbxproj @@ -485,7 +485,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -542,7 +542,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; From b445bfda13bc93d73fcedcc86ab57e59da5c310e Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Wed, 22 Apr 2026 17:37:32 -0400 Subject: [PATCH 05/12] Add @MainActor to Rx+ManualDateField for concurrency safety --- Cordate/Sources/Rx/Rx+ManualDateField.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Cordate/Sources/Rx/Rx+ManualDateField.swift b/Cordate/Sources/Rx/Rx+ManualDateField.swift index ab4c472..307213f 100644 --- a/Cordate/Sources/Rx/Rx+ManualDateField.swift +++ b/Cordate/Sources/Rx/Rx+ManualDateField.swift @@ -2,6 +2,7 @@ import Foundation @preconcurrency import RxCocoa @preconcurrency import RxSwift +@MainActor public extension Reactive where Base: ManualDateField { var date: ControlProperty { From 5d545671b555600e0cf0eb1763de3bb986eded1d Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Wed, 22 Apr 2026 17:38:06 -0400 Subject: [PATCH 06/12] Fix MainActor isolation in Rx closures for Xcode 16.4 compatibility --- Cordate/Sources/Rx/Rx+Calendar.swift | 14 ++++++++------ Cordate/Sources/Rx/Rx+ManualDateField.swift | 6 ++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Cordate/Sources/Rx/Rx+Calendar.swift b/Cordate/Sources/Rx/Rx+Calendar.swift index 1eed825..4eca5db 100644 --- a/Cordate/Sources/Rx/Rx+Calendar.swift +++ b/Cordate/Sources/Rx/Rx+Calendar.swift @@ -15,24 +15,26 @@ public extension Reactive where Base: CalendarDateSelectionController { } var selectedDate: ControlEvent { - let proxy = base.delegate as? CalendarDelegateProxy ?? installProxy() + let proxy = MainActor.assumeIsolated { base.delegate as? CalendarDelegateProxy } ?? installProxy() let source = proxy.rx.methodInvoked(#selector(CalendarDateSelectionControllerDelegate.calendarController(_:didSelectDate:))) .map { args in args[1] as! Date } return ControlEvent(events: source) } var clearedDate: ControlEvent { - let proxy = base.delegate as? CalendarDelegateProxy ?? installProxy() + let proxy = MainActor.assumeIsolated { base.delegate as? CalendarDelegateProxy } ?? installProxy() let source = proxy.rx.methodInvoked(#selector(CalendarDateSelectionControllerDelegate.calendarControllerClearedDate(_:))) .map { _ in () } return ControlEvent(events: source) } private func installProxy() -> CalendarDelegateProxy { - let proxy = CalendarDelegateProxy(forwardedDelegate: base.delegate) - base.delegate = proxy - objc_setAssociatedObject(base, &delegateProxyKey, proxy, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return proxy + return MainActor.assumeIsolated { + let proxy = CalendarDelegateProxy(forwardedDelegate: base.delegate) + base.delegate = proxy + objc_setAssociatedObject(base, &delegateProxyKey, proxy, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return proxy + } } } diff --git a/Cordate/Sources/Rx/Rx+ManualDateField.swift b/Cordate/Sources/Rx/Rx+ManualDateField.swift index 307213f..304aee9 100644 --- a/Cordate/Sources/Rx/Rx+ManualDateField.swift +++ b/Cordate/Sources/Rx/Rx+ManualDateField.swift @@ -6,9 +6,11 @@ import Foundation public extension Reactive where Base: ManualDateField { var date: ControlProperty { - let source = base.rx.text.map { _ in self.base.date } + let source = base.rx.text.map { [weak base] _ in + MainActor.assumeIsolated { base?.date } + } let sink = Binder(base) { field, date in - field.setDate(date) + MainActor.assumeIsolated { field.setDate(date) } } return ControlProperty(values: source, valueSink: sink) } From 27c8fe28f22b1b00b0d89b120b72552f006dc5af Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Thu, 23 Apr 2026 13:21:13 -0400 Subject: [PATCH 07/12] Address PR feedback: update to Xcode 26.3, swift-tools-version 6.2, restore removed comments, remove @preconcurrency imports, fix whitespace artifacts --- .github/workflows/ci.yml | 4 +- .../CalendarDateSelectionController.swift | 23 +++++++++++ .../Sources/Core/Calendar/CalendarStyle.swift | 41 +++++++++++++++++++ .../CalendarPresentationController.swift | 6 +++ .../ManualDateField/ManualDateField.swift | 3 -- Cordate/Sources/Rx/Rx+Calendar.swift | 7 +++- Cordate/Sources/Rx/Rx+ManualDateField.swift | 6 ++- Package.swift | 2 +- README.md | 2 +- 9 files changed, 83 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fb6f6b..f23a1c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,6 @@ jobs: steps: - uses: actions/checkout@v4 - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_26.3.app/Contents/Developer - name: Build - run: xcodebuild build -scheme Cordate -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' + run: xcodebuild build -scheme Cordate -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' diff --git a/Cordate/Sources/Core/Calendar/CalendarDateSelectionController.swift b/Cordate/Sources/Core/Calendar/CalendarDateSelectionController.swift index 8d14b17..2c40ff2 100644 --- a/Cordate/Sources/Core/Calendar/CalendarDateSelectionController.swift +++ b/Cordate/Sources/Core/Calendar/CalendarDateSelectionController.swift @@ -1,9 +1,16 @@ import Foundation import UIKit +/// Adopters of the 'CalendarDateSelectionControllerDelegate` respond to events triggered by user +/// interaction with the calendar. @objc public protocol CalendarDateSelectionControllerDelegate: UICollectionViewDelegate { + + /// Called by the calendar when a user cleared the date. @objc optional func calendarControllerClearedDate(_ controller: CalendarDateSelectionController) + + /// Called by calendar when a user selects a date. @objc optional func calendarController(_ controller: CalendarDateSelectionController, didSelectDate date: Date) + } public class CalendarDateSelectionController: UIViewController { @@ -12,9 +19,16 @@ public class CalendarDateSelectionController: UIViewController { private static let reuseIdentifier = "com.Cordate.calendar" + /// Returns the title shown at the top of the calendar. public let calendarTitle: String? + + /// Returns the data source driving the calendar. public let dataSource: CalendarDataSource + + /// Returns the view which renders the calendar components. public let calendarView: UICollectionView + + /// Returns the layout object which drives the calendar layout. public let calendarLayout = CalendarLayout() private let titleLabel = UILabel() @@ -24,10 +38,12 @@ public class CalendarDateSelectionController: UIViewController { private let clearButton = UIButton(type: .system) private let confirmButton = UIButton(type: .system) + /// The currently-selected date. @objc dynamic public var date = Date?.none { didSet { confirmButton.isEnabled = date != nil } } + /// The style collection for the calendar. public var style = CalendarStyle() { didSet { applyStyle() } } @@ -36,6 +52,7 @@ public class CalendarDateSelectionController: UIViewController { return CalendarPresentationController(presentedViewController: self, presenting: nil) }() + /// The delegate which receives events from the calendar. public weak var delegate = CalendarDateSelectionControllerDelegate?.none private var currentComponent = CalendarDataSource.Component.year { @@ -64,6 +81,8 @@ public class CalendarDateSelectionController: UIViewController { private var selectedDay = Int?.none + /// Performs an initial layout pass in viewDidLayoutSubviews which prepares the collection view's + /// layout and, if applicable, scrolls to the selected year. private var hasLaidOutOnce = false public init(dataSource: CalendarDataSource = CalendarDataSource(), title: String? = nil, initialDate: Date? = nil) { @@ -271,6 +290,8 @@ public class CalendarDateSelectionController: UIViewController { } private func triggerReload() { + // TODO : Determine how to set the content offset or scroll to an item as part of an animated + // reload. The content offset/item to scroll to needs set up prior to animating the cells. UIView.animate(withDuration: 0.2, animations: { self.calendarView.alpha = 0 self.calendarHeader.alpha = 0 @@ -391,6 +412,8 @@ extension CalendarDateSelectionController: UICollectionViewDelegate { currentComponent = .month(year: selectedYear!) generator.selectionChanged() guard let selectedCell: ComponentCell = collectionView.cell(for: indexPath) else { + // This and the other similar cases should never happen for obvious reasons, but in + // the event that it does this will ensure that usability isn't totally hampered. triggerReload() return } diff --git a/Cordate/Sources/Core/Calendar/CalendarStyle.swift b/Cordate/Sources/Core/Calendar/CalendarStyle.swift index 2438bde..9ec27b0 100644 --- a/Cordate/Sources/Core/Calendar/CalendarStyle.swift +++ b/Cordate/Sources/Core/Calendar/CalendarStyle.swift @@ -19,24 +19,65 @@ public struct CalendarStyle: Sendable { return .white } + /// Drives which buttons are shown on the calendar. public enum ButtonConfiguration: Sendable { + + /// The calendar shows no buttons and produces values continuously. case none + + /// The calendar shows a confirmation button and only produces a value when it is pressed. case confirmationOnly + + /// The calendar shows both a confirmation button and a clear button. case all + } + /// The background color for unselected date components. public var unselectedBackgroundColor = UIColor(white: 0.95, alpha: 1) + + /// The text color for unselected date components. + /// + /// When this value is `nil`, the calendar will choose either white or black text depending on + /// the intensity of the current background color. public var unselectedTextColor = UIColor?.none + + /// The text color for selected date components. + /// + /// When this value is `nil`, the calendar will choose either white or black text depending on + /// the intensity of the current background color. public var selectedTextColor = UIColor?.none + + /// The background color for selected date components. + /// + /// When this value is `nil`, the calendar will use its view's tint color. public var selectedBackgroundColor = UIColor?.none + + /// The text color for the day-mode header. public var headerTextColor = UIColor.black + + /// The tint color for the calendar mode buttons in the enabled state. + /// + /// When this value is `nil`, the calendar will use its view's tint color. public var enabledCalendarModeButtonColor = UIColor?.none + + /// The tint color for the calendar mode buttons in the disabled state. public var disabledCalendarModeButtonColor = UIColor(white: 0.9, alpha: 1) + + /// The font for the calendar mode buttons. public var calendarModeButtonFont = UIFont.boldSystemFont(ofSize: 16) + public var titleFont = UIFont.boldSystemFont(ofSize: 18) + public var calendarHeaderFont = UIFont.boldSystemFont(ofSize: 14) + + /// The font for the calendar controls. public var calendarFont = UIFont.systemFont(ofSize: UIFont.systemFontSize) + + /// Controls whether selection events trigger haptic feedback. public var usesHaptics: Bool = true + + /// Controls which controls are provided by the calendar. public var buttonConfiguration = ButtonConfiguration.all public init() { } diff --git a/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift b/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift index d478c97..68ce1b7 100644 --- a/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift +++ b/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift @@ -1,13 +1,19 @@ import Foundation import UIKit +/// The `CalendarPresentationController` class is a subclass of `UIPresentationController` which +/// provides default behaviors and styling for the `CalendarDateSelectionController`. public class CalendarPresentationController: UIPresentationController { private let blurEffectView = UIVisualEffectView() private let tapGestureRecognizer = UITapGestureRecognizer() + /// The blur effect used by the overlay. public var blurEffect = UIBlurEffect(style: .regular) + /// Controls whether touches on the blur overlay trigger a dismissal. + /// + /// The default value of this is `true`. public var overlayTouchesShouldDismiss: Bool = true { didSet { tapGestureRecognizer.isEnabled = overlayTouchesShouldDismiss diff --git a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift index 9437309..8429827 100644 --- a/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift +++ b/Cordate/Sources/Core/Controls/ManualDateField/ManualDateField.swift @@ -110,9 +110,6 @@ public class ManualDateField: UITextField { MainActor.assumeIsolated { text.map { formatText($0) } ?? "" } } - - - public override func drawText(in rect: CGRect) { let drawingRepresentation = generateDrawingRepresentation() let heightInset = (rect.size.height - drawingRepresentation.size().height) / 2 diff --git a/Cordate/Sources/Rx/Rx+Calendar.swift b/Cordate/Sources/Rx/Rx+Calendar.swift index 4eca5db..38d17bb 100644 --- a/Cordate/Sources/Rx/Rx+Calendar.swift +++ b/Cordate/Sources/Rx/Rx+Calendar.swift @@ -1,12 +1,13 @@ import Foundation -@preconcurrency import RxCocoa -@preconcurrency import RxSwift +import RxCocoa +import RxSwift nonisolated(unsafe) fileprivate var delegateProxyKey = UInt8.max @MainActor public extension Reactive where Base: CalendarDateSelectionController { + /// Returns a sequence which emits changes to the selected date. var date: ControlEvent { let source = observe(Date?.self, #keyPath(CalendarDateSelectionController.date)) .filter { $0 != nil } @@ -14,6 +15,7 @@ public extension Reactive where Base: CalendarDateSelectionController { return ControlEvent(events: source) } + /// Returns a sequence which emits explicit confirmations of the selected date. var selectedDate: ControlEvent { let proxy = MainActor.assumeIsolated { base.delegate as? CalendarDelegateProxy } ?? installProxy() let source = proxy.rx.methodInvoked(#selector(CalendarDateSelectionControllerDelegate.calendarController(_:didSelectDate:))) @@ -21,6 +23,7 @@ public extension Reactive where Base: CalendarDateSelectionController { return ControlEvent(events: source) } + /// Returns a sequence which emits explicit removals of the selected date. var clearedDate: ControlEvent { let proxy = MainActor.assumeIsolated { base.delegate as? CalendarDelegateProxy } ?? installProxy() let source = proxy.rx.methodInvoked(#selector(CalendarDateSelectionControllerDelegate.calendarControllerClearedDate(_:))) diff --git a/Cordate/Sources/Rx/Rx+ManualDateField.swift b/Cordate/Sources/Rx/Rx+ManualDateField.swift index 304aee9..4ab169a 100644 --- a/Cordate/Sources/Rx/Rx+ManualDateField.swift +++ b/Cordate/Sources/Rx/Rx+ManualDateField.swift @@ -1,10 +1,12 @@ import Foundation -@preconcurrency import RxCocoa -@preconcurrency import RxSwift +import RxCocoa +import RxSwift @MainActor public extension Reactive where Base: ManualDateField { + /// Returns a Reactive control property which emits changes to the field's date and accepts new + /// dates. var date: ControlProperty { let source = base.rx.text.map { [weak base] _ in MainActor.assumeIsolated { base?.date } diff --git a/Package.swift b/Package.swift index 4cad3ea..7d00c43 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.0 +// swift-tools-version:6.2 import PackageDescription diff --git a/README.md b/README.md index 8d994e4..3eb5b1b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Cordate is a small library which makes working with dates much smoother by addin Swift Package Manager: ```swift -// swift-tools-version:6.0 +// swift-tools-version:6.2 import PackageDescription From eeb08a5e36a1fd4c269afd2d3a0016688c682e45 Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Thu, 23 Apr 2026 14:09:06 -0400 Subject: [PATCH 08/12] Pin RxSwift to exact version 6.5.0 --- Package.resolved | 6 +++--- Package.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.resolved b/Package.resolved index 7b03fe5..cee114f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "fdd55bcafe2d5972feb5c769038b0b53971f4508152cbc7eb342010165d417b5", + "originHash" : "eec5220335a6a84926bff622f1eca793787207b05a2e56b3e5c5f3f45454dd68", "pins" : [ { "identity" : "rxswift", "kind" : "remoteSourceControl", "location" : "https://github.com/ReactiveX/RxSwift.git", "state" : { - "revision" : "132aea4f236ccadc51590b38af0357a331d51fa2", - "version" : "6.10.2" + "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8", + "version" : "6.5.0" } } ], diff --git a/Package.swift b/Package.swift index 7d00c43..37bd571 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( targets: ["Cordate"]), ], dependencies: [ - .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.5.0") + .package(url: "https://github.com/ReactiveX/RxSwift.git", exact: "6.5.0") ], targets: [ .target( From 10146c65f069dfd390db792d33917aa8075bbd8a Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Fri, 24 Apr 2026 12:16:17 -0400 Subject: [PATCH 09/12] fix: use @unchecked Sendable for CalendarStyle (contains non-Sendable UIColor/UIFont) --- Cordate/Sources/Core/Calendar/CalendarStyle.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cordate/Sources/Core/Calendar/CalendarStyle.swift b/Cordate/Sources/Core/Calendar/CalendarStyle.swift index 9ec27b0..5681460 100644 --- a/Cordate/Sources/Core/Calendar/CalendarStyle.swift +++ b/Cordate/Sources/Core/Calendar/CalendarStyle.swift @@ -1,7 +1,7 @@ import Foundation import UIKit -public struct CalendarStyle: Sendable { +public struct CalendarStyle: @unchecked Sendable { static func textColor(for color: UIColor) -> UIColor? { guard color != .clear else { return .black } From ad448d81342cb97d09a829428e2a6ee0f43fa7da Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Fri, 24 Apr 2026 12:26:15 -0400 Subject: [PATCH 10/12] Remove redundant MainActor.assumeIsolated calls and unnecessary @unchecked Sendable on CalendarStyle --- Cordate/Sources/Core/Calendar/CalendarStyle.swift | 2 +- Cordate/Sources/Rx/Rx+Calendar.swift | 14 ++++++-------- Cordate/Sources/Rx/Rx+ManualDateField.swift | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Cordate/Sources/Core/Calendar/CalendarStyle.swift b/Cordate/Sources/Core/Calendar/CalendarStyle.swift index 5681460..b1b3594 100644 --- a/Cordate/Sources/Core/Calendar/CalendarStyle.swift +++ b/Cordate/Sources/Core/Calendar/CalendarStyle.swift @@ -1,7 +1,7 @@ import Foundation import UIKit -public struct CalendarStyle: @unchecked Sendable { +public struct CalendarStyle { static func textColor(for color: UIColor) -> UIColor? { guard color != .clear else { return .black } diff --git a/Cordate/Sources/Rx/Rx+Calendar.swift b/Cordate/Sources/Rx/Rx+Calendar.swift index 38d17bb..3a4f1ec 100644 --- a/Cordate/Sources/Rx/Rx+Calendar.swift +++ b/Cordate/Sources/Rx/Rx+Calendar.swift @@ -17,7 +17,7 @@ public extension Reactive where Base: CalendarDateSelectionController { /// Returns a sequence which emits explicit confirmations of the selected date. var selectedDate: ControlEvent { - let proxy = MainActor.assumeIsolated { base.delegate as? CalendarDelegateProxy } ?? installProxy() + let proxy = (base.delegate as? CalendarDelegateProxy) ?? installProxy() let source = proxy.rx.methodInvoked(#selector(CalendarDateSelectionControllerDelegate.calendarController(_:didSelectDate:))) .map { args in args[1] as! Date } return ControlEvent(events: source) @@ -25,19 +25,17 @@ public extension Reactive where Base: CalendarDateSelectionController { /// Returns a sequence which emits explicit removals of the selected date. var clearedDate: ControlEvent { - let proxy = MainActor.assumeIsolated { base.delegate as? CalendarDelegateProxy } ?? installProxy() + let proxy = (base.delegate as? CalendarDelegateProxy) ?? installProxy() let source = proxy.rx.methodInvoked(#selector(CalendarDateSelectionControllerDelegate.calendarControllerClearedDate(_:))) .map { _ in () } return ControlEvent(events: source) } private func installProxy() -> CalendarDelegateProxy { - return MainActor.assumeIsolated { - let proxy = CalendarDelegateProxy(forwardedDelegate: base.delegate) - base.delegate = proxy - objc_setAssociatedObject(base, &delegateProxyKey, proxy, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return proxy - } + let proxy = CalendarDelegateProxy(forwardedDelegate: base.delegate) + base.delegate = proxy + objc_setAssociatedObject(base, &delegateProxyKey, proxy, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return proxy } } diff --git a/Cordate/Sources/Rx/Rx+ManualDateField.swift b/Cordate/Sources/Rx/Rx+ManualDateField.swift index 4ab169a..cb3e8ef 100644 --- a/Cordate/Sources/Rx/Rx+ManualDateField.swift +++ b/Cordate/Sources/Rx/Rx+ManualDateField.swift @@ -9,10 +9,10 @@ public extension Reactive where Base: ManualDateField { /// dates. var date: ControlProperty { let source = base.rx.text.map { [weak base] _ in - MainActor.assumeIsolated { base?.date } + base?.date } let sink = Binder(base) { field, date in - MainActor.assumeIsolated { field.setDate(date) } + field.setDate(date) } return ControlProperty(values: source, valueSink: sink) } From 04257eb55cf57a3d7fb28036277af50bce440e67 Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Tue, 28 Apr 2026 17:33:51 -0400 Subject: [PATCH 11/12] Fix CI: remove master branch, unnecessary xcode-select, use macos-latest --- .github/workflows/ci.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f23a1c9..bfa0468 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,16 +2,14 @@ name: CI on: push: - branches: [main, master] + branches: [main] pull_request: - branches: [main, master] + branches: [main] jobs: build: - runs-on: macos-15 + runs-on: macos-latest steps: - uses: actions/checkout@v4 - - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_26.3.app/Contents/Developer - name: Build - run: xcodebuild build -scheme Cordate -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' + run: xcodebuild build -scheme Cordate -destination 'platform=iOS Simulator,name=iPhone 16' From 384fd977e7614ac79167690923714a035e9ca86b Mon Sep 17 00:00:00 2001 From: Venkata Krishna Garapati Date: Tue, 28 Apr 2026 17:34:23 -0400 Subject: [PATCH 12/12] Fix CI: remove master branch, unnecessary xcode-select, use macos-latest --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfa0468..615be42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,4 +12,4 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build - run: xcodebuild build -scheme Cordate -destination 'platform=iOS Simulator,name=iPhone 16' + run: xcodebuild build -scheme Cordate -destination 'platform=iOS Simulator,name=iPhone 17'