diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..615be42 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,15 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: xcodebuild build -scheme Cordate -destination 'platform=iOS Simulator,name=iPhone 17' 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..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; @@ -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..2c40ff2 100644 --- a/Cordate/Sources/Core/Calendar/CalendarDateSelectionController.swift +++ b/Cordate/Sources/Core/Calendar/CalendarDateSelectionController.swift @@ -78,7 +78,7 @@ 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 @@ -399,7 +399,7 @@ extension CalendarDateSelectionController: UICollectionViewDelegate { } public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let generator = SelectionFeedbackGenerator() + let generator = UISelectionFeedbackGenerator() generator.prepare() switch currentComponent { case .year: @@ -497,27 +497,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..b1b3594 100644 --- a/Cordate/Sources/Core/Calendar/CalendarStyle.swift +++ b/Cordate/Sources/Core/Calendar/CalendarStyle.swift @@ -20,7 +20,7 @@ public struct CalendarStyle { } /// Drives which buttons are shown on the calendar. - public enum ButtonConfiguration { + public enum ButtonConfiguration: Sendable { /// The calendar shows no buttons and produces values continuously. case none @@ -28,7 +28,7 @@ public struct CalendarStyle { /// 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. + /// The calendar shows both a confirmation button and a clear button. case all } @@ -74,14 +74,8 @@ public struct CalendarStyle { /// 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 } - } + public var usesHaptics: Bool = true /// Controls which controls are provided by the calendar. public var buttonConfiguration = ButtonConfiguration.all diff --git a/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift b/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift index c6b7df2..68ce1b7 100644 --- a/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift +++ b/Cordate/Sources/Core/Calendar/Transitions/CalendarPresentationController.swift @@ -1,11 +1,6 @@ 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 { @@ -14,8 +9,7 @@ public class CalendarPresentationController: UIPresentationController { 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. /// @@ -73,4 +67,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..8429827 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) ?? "" + MainActor.assumeIsolated { 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..3a4f1ec 100644 --- a/Cordate/Sources/Rx/Rx+Calendar.swift +++ b/Cordate/Sources/Rx/Rx+Calendar.swift @@ -2,8 +2,9 @@ import Foundation import RxCocoa 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. @@ -16,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 = 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) @@ -24,7 +25,7 @@ public extension Reactive where Base: CalendarDateSelectionController { /// Returns a sequence which emits explicit removals of the selected date. var clearedDate: ControlEvent { - let proxy = 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) @@ -39,6 +40,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..cb3e8ef 100644 --- a/Cordate/Sources/Rx/Rx+ManualDateField.swift +++ b/Cordate/Sources/Rx/Rx+ManualDateField.swift @@ -2,12 +2,15 @@ import Foundation 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 { _ in self.base.date } + let source = base.rx.text.map { [weak base] _ in + base?.date + } let sink = Binder(base) { field, date in field.setDate(date) } diff --git a/Package.resolved b/Package.resolved index a785239..cee114f 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" : "eec5220335a6a84926bff622f1eca793787207b05a2e56b3e5c5f3f45454dd68", + "pins" : [ + { + "identity" : "rxswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactiveX/RxSwift.git", + "state" : { + "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8", + "version" : "6.5.0" } - ] - }, - "version": 1 + } + ], + "version" : 3 } diff --git a/Package.swift b/Package.swift index b6af892..37bd571 100644 --- a/Package.swift +++ b/Package.swift @@ -1,22 +1,25 @@ -// swift-tools-version:5.1 +// swift-tools-version:6.2 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", exact: "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..3eb5b1b 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.2 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.