@swiftbrowser/swift 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +23 -0
- package/README.md +12 -2
- package/Sources/Charts/Charts.swift +65 -0
- package/Sources/FoundationBridge/FoundationBridge.swift +77 -0
- package/Sources/FoundationBridge/Locale.swift +86 -0
- package/Sources/FoundationBridge/Timer.swift +48 -0
- package/Sources/FoundationBridge/UITextChecker.swift +30 -0
- package/Sources/FoundationLite/Common.swift +3 -0
- package/Sources/FoundationLite/FoundationLite.swift +171 -0
- package/Sources/FoundationLite/Interaction.swift +146 -0
- package/Sources/FoundationLite/Timer.swift +48 -0
- package/Sources/SwiftUI/API/Accessibility.swift +132 -0
- package/Sources/SwiftUI/API/Alert.swift +221 -0
- package/Sources/SwiftUI/API/Animation.swift +253 -0
- package/Sources/SwiftUI/API/App.swift +57 -0
- package/Sources/SwiftUI/API/Color.swift +161 -0
- package/Sources/SwiftUI/API/Controls.swift +268 -0
- package/Sources/SwiftUI/API/CustomLayout.swift +403 -0
- package/Sources/SwiftUI/API/Decorations.swift +92 -0
- package/Sources/SwiftUI/API/EditMode.swift +269 -0
- package/Sources/SwiftUI/API/Effects.swift +290 -0
- package/Sources/SwiftUI/API/Environment.swift +355 -0
- package/Sources/SwiftUI/API/Focus.swift +129 -0
- package/Sources/SwiftUI/API/Font.swift +131 -0
- package/Sources/SwiftUI/API/Gauge.swift +289 -0
- package/Sources/SwiftUI/API/Geometry.swift +89 -0
- package/Sources/SwiftUI/API/GeometryReader.swift +124 -0
- package/Sources/SwiftUI/API/Gestures.swift +342 -0
- package/Sources/SwiftUI/API/Grid.swift +188 -0
- package/Sources/SwiftUI/API/LabelStyle.swift +117 -0
- package/Sources/SwiftUI/API/Layout.swift +165 -0
- package/Sources/SwiftUI/API/Lists.swift +276 -0
- package/Sources/SwiftUI/API/Menu.swift +51 -0
- package/Sources/SwiftUI/API/Modifiers.swift +243 -0
- package/Sources/SwiftUI/API/Navigation.swift +371 -0
- package/Sources/SwiftUI/API/ObservableObject.swift +279 -0
- package/Sources/SwiftUI/API/OnChange.swift +48 -0
- package/Sources/SwiftUI/API/Phase8Controls.swift +394 -0
- package/Sources/SwiftUI/API/Picker.swift +176 -0
- package/Sources/SwiftUI/API/Presentation.swift +109 -0
- package/Sources/SwiftUI/API/Primitives.swift +390 -0
- package/Sources/SwiftUI/API/Publishers.swift +107 -0
- package/Sources/SwiftUI/API/RowActions.swift +83 -0
- package/Sources/SwiftUI/API/ScrollTargets.swift +104 -0
- package/Sources/SwiftUI/API/ShapeStyle.swift +311 -0
- package/Sources/SwiftUI/API/State.swift +127 -0
- package/Sources/SwiftUI/API/StrokeStyle.swift +30 -0
- package/Sources/SwiftUI/API/Tabs.swift +119 -0
- package/Sources/SwiftUI/API/Tasks.swift +35 -0
- package/Sources/SwiftUI/API/Timeline.swift +212 -0
- package/Sources/SwiftUI/API/Toolbar.swift +215 -0
- package/Sources/SwiftUI/API/View.swift +207 -0
- package/Sources/SwiftUI/API/ViewModifier.swift +40 -0
- package/Sources/SwiftUI/API/VisualEffect.swift +82 -0
- package/Sources/SwiftUI/Charts/Axis.swift +319 -0
- package/Sources/SwiftUI/Charts/Chart.swift +646 -0
- package/Sources/SwiftUI/Charts/ChartContent.swift +376 -0
- package/Sources/SwiftUI/Charts/Marks.swift +204 -0
- package/Sources/SwiftUI/Charts/Plottable.swift +99 -0
- package/Sources/SwiftUI/Charts/Scales.swift +326 -0
- package/Sources/SwiftUI/Runtime/Executor.swift +83 -0
- package/Sources/SwiftUI/Runtime/Exports.swift +65 -0
- package/Sources/SwiftUI/Runtime/JSON.swift +71 -0
- package/Sources/SwiftUI/Runtime/JSONParser.swift +443 -0
- package/Sources/SwiftUI/Runtime/Ops.swift +106 -0
- package/Sources/SwiftUI/Runtime/Reconciler.swift +210 -0
- package/Sources/SwiftUI/Runtime/Runtime.swift +1367 -0
- package/Sources/_FoundationCommon/Bundle.swift +82 -0
- package/Sources/_FoundationCommon/UserDefaults.swift +170 -0
- package/Sources/_FoundationCommon/_FoundationCommon.swift +10 -0
- package/index.d.ts +2 -0
- package/index.mjs +16 -0
- package/package.json +28 -4
- package/sync-sources.mjs +19 -0
package/Package.swift
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// swift-tools-version: 6.0
|
|
2
|
+
// The SwiftBrowser Swift package as @swiftbrowser/cli ships it: the SwiftUI
|
|
3
|
+
// shim, Swift Charts, FoundationLite and FoundationBridge, library products only. `Sources/` is
|
|
4
|
+
// copied from the repository's Sources/ at `npm pack` time (sync-sources.mjs);
|
|
5
|
+
// in a repository checkout the CLI uses the repository's own Package.swift.
|
|
6
|
+
import PackageDescription
|
|
7
|
+
|
|
8
|
+
let package = Package(
|
|
9
|
+
name: "SwiftBrowser",
|
|
10
|
+
products: [
|
|
11
|
+
.library(name: "SwiftUI", targets: ["SwiftUI"]),
|
|
12
|
+
.library(name: "Charts", targets: ["Charts"]),
|
|
13
|
+
.library(name: "FoundationLite", targets: ["FoundationLite"]),
|
|
14
|
+
.library(name: "FoundationBridge", targets: ["FoundationBridge"]),
|
|
15
|
+
],
|
|
16
|
+
targets: [
|
|
17
|
+
.target(name: "SwiftUI", path: "Sources/SwiftUI"),
|
|
18
|
+
.target(name: "_FoundationCommon", dependencies: ["SwiftUI"], path: "Sources/_FoundationCommon"),
|
|
19
|
+
.target(name: "FoundationLite", dependencies: ["SwiftUI", "_FoundationCommon"], path: "Sources/FoundationLite"),
|
|
20
|
+
.target(name: "FoundationBridge", dependencies: ["SwiftUI", "_FoundationCommon"], path: "Sources/FoundationBridge"),
|
|
21
|
+
.target(name: "Charts", dependencies: ["SwiftUI"], path: "Sources/Charts"),
|
|
22
|
+
]
|
|
23
|
+
)
|
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @swiftbrowser/swift
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The SwiftBrowser Swift package (the `SwiftUI` shim, `Charts`, `FoundationLite`
|
|
4
|
+
for lean builds, `FoundationBridge` for full-Foundation builds and the
|
|
5
|
+
`_FoundationCommon` they share) packaged for npm, so that `@swiftbrowser/cli` builds apps
|
|
6
|
+
against exactly the shim version it was released with. The CLI's generated
|
|
7
|
+
staging package depends on it by path (`swiftPackagePath()`).
|
|
8
|
+
|
|
9
|
+
In a repository checkout `Sources/` is not here; `swiftPackagePath()` then
|
|
10
|
+
points at the repository root, whose package also declares the example
|
|
11
|
+
executables (so an app named like one of them, `Counter` say, needs this
|
|
12
|
+
package synced: `node packages/swift/sync-sources.mjs`). `npm pack` copies the
|
|
13
|
+
sources in (`prepack`).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Swift Charts for SwiftBrowser. The marks, axes and `Chart` itself are
|
|
2
|
+
// defined in the SwiftUI target (Sources/SwiftUI/Charts); this module
|
|
3
|
+
// re-exports them and adds the pieces that need Foundation's `Date` and
|
|
4
|
+
// `Calendar`. See docs/ops-protocol.md, "Phase 7".
|
|
5
|
+
|
|
6
|
+
@_exported import SwiftUI
|
|
7
|
+
|
|
8
|
+
#if canImport(FoundationEssentials)
|
|
9
|
+
import FoundationEssentials
|
|
10
|
+
#else
|
|
11
|
+
import Foundation
|
|
12
|
+
#endif
|
|
13
|
+
|
|
14
|
+
/// Dates plot as seconds since 1970; the chart treats them as UTC
|
|
15
|
+
/// (`Calendar.current` is UTC in the browser).
|
|
16
|
+
extension Date: Plottable {
|
|
17
|
+
public var _plottableValue: _PlottableValue { .date(timeIntervalSince1970) }
|
|
18
|
+
|
|
19
|
+
public init?(_plottableValue value: _PlottableValue) {
|
|
20
|
+
guard case .date(let seconds) = value else { return nil }
|
|
21
|
+
self.init(timeIntervalSince1970: seconds)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Calendar bins over a date range: `DateBins(unit: .hour, by: 3, range:)`.
|
|
26
|
+
/// `thresholds` are the bin boundaries, from the range's start (aligned to
|
|
27
|
+
/// `unit`) in steps of `by` units up to and including the first boundary at or
|
|
28
|
+
/// after the range's end.
|
|
29
|
+
public struct DateBins: Sendable {
|
|
30
|
+
public let unit: Calendar.Component
|
|
31
|
+
public let stride: Int
|
|
32
|
+
public let range: ClosedRange<Date>
|
|
33
|
+
public let thresholds: [Date]
|
|
34
|
+
|
|
35
|
+
public init(unit: Calendar.Component, by stride: Int = 1, range: ClosedRange<Date>,
|
|
36
|
+
calendar: Calendar = .current) {
|
|
37
|
+
self.unit = unit
|
|
38
|
+
self.stride = max(1, stride)
|
|
39
|
+
self.range = range
|
|
40
|
+
var thresholds: [Date] = []
|
|
41
|
+
var date = calendar.dateInterval(of: unit, for: range.lowerBound)?.start ?? range.lowerBound
|
|
42
|
+
while thresholds.count < 10_000 {
|
|
43
|
+
thresholds.append(date)
|
|
44
|
+
if date >= range.upperBound { break }
|
|
45
|
+
guard let next = calendar.date(byAdding: unit, value: self.stride, to: date), next > date else { break }
|
|
46
|
+
date = next
|
|
47
|
+
}
|
|
48
|
+
self.thresholds = thresholds
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Bins over the dates' span.
|
|
52
|
+
public init(unit: Calendar.Component, by stride: Int = 1, dates: [Date], calendar: Calendar = .current) {
|
|
53
|
+
let lower = dates.min() ?? Date(timeIntervalSince1970: 0)
|
|
54
|
+
let upper = dates.max() ?? lower
|
|
55
|
+
self.init(unit: unit, by: stride, range: lower...upper, calendar: calendar)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// The bin a date falls in (an index into `thresholds`), clamped to the bins.
|
|
59
|
+
public func index(for date: Date) -> Int {
|
|
60
|
+
let bins = max(thresholds.count - 1, 1)
|
|
61
|
+
var index = 0
|
|
62
|
+
for (i, threshold) in thresholds.enumerated() where date >= threshold { index = i }
|
|
63
|
+
return min(index, bins - 1)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/// What the SwiftUI shim needs from the real Foundation, for apps built with
|
|
2
|
+
/// `--foundation full`: the SDK's swift-corelibs Foundation (with ICU data
|
|
3
|
+
/// trimmed by the CLI) instead of FoundationEssentials + FoundationLite.
|
|
4
|
+
///
|
|
5
|
+
/// The shim itself never imports Foundation (that would link its 10 MB into
|
|
6
|
+
/// every lean app), so it is generic over small protocols that this module
|
|
7
|
+
/// conforms Foundation's types to: `IndexSet` for `onDelete` / `onMove` /
|
|
8
|
+
/// `remove(atOffsets:)`, `Date` for `DatePicker`, and `FormatStyle` /
|
|
9
|
+
/// `ParseableFormatStyle` for `Text(_:format:)` and `TextField(_:value:format:)`.
|
|
10
|
+
/// `Timer.swift` adds the `Timer` and `RunLoop` that corelibs Foundation does
|
|
11
|
+
/// not have on WASI.
|
|
12
|
+
///
|
|
13
|
+
/// Apps do not import this module themselves: the CLI's staging adds it next
|
|
14
|
+
/// to `import Foundation`, together with typealiases that settle the names
|
|
15
|
+
/// both the shim and Foundation declare (`CGFloat`, `CGPoint`, `CGSize`,
|
|
16
|
+
/// `CGRect`, `Timer`, `RunLoop`) in the shim's favor. See
|
|
17
|
+
/// docs/ops-protocol.md, "Phase 11".
|
|
18
|
+
import Foundation
|
|
19
|
+
import SwiftUI
|
|
20
|
+
import _FoundationCommon
|
|
21
|
+
|
|
22
|
+
// MARK: - Offset sets
|
|
23
|
+
|
|
24
|
+
extension IndexSet: _OffsetSet {
|
|
25
|
+
public init(_offsets: [Int]) { self.init(_offsets) }
|
|
26
|
+
/// `IndexSet` iterates its integers in ascending order without duplicates.
|
|
27
|
+
public var _offsets: [Int] { Array(self) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MARK: - Dates
|
|
31
|
+
|
|
32
|
+
extension Date: _DatePickerValue {
|
|
33
|
+
public var _secondsSince1970: Double { timeIntervalSince1970 }
|
|
34
|
+
public init(_secondsSince1970 seconds: Double) {
|
|
35
|
+
self.init(timeIntervalSince1970: seconds)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// MARK: - Format styles
|
|
40
|
+
|
|
41
|
+
/// A Foundation parseable format style as the shim's `_TextFormat`.
|
|
42
|
+
struct _ParseableFormatStyleText<F: ParseableFormatStyle>: _TextFormat where F.FormatOutput == String, F.Strategy.ParseInput == String {
|
|
43
|
+
let style: F
|
|
44
|
+
func _formatText(_ value: F.FormatInput) -> String { style.format(value) }
|
|
45
|
+
func _parseText(_ text: String) -> F.FormatInput? { try? style.parseStrategy.parse(text) }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
extension Text {
|
|
49
|
+
/// `Text(price, format: .currency(code: "USD"))`, `Text(date, format: .dateTime.year())`.
|
|
50
|
+
public init<F: FormatStyle>(_ input: F.FormatInput, format: F) where F.FormatOutput == String {
|
|
51
|
+
self.init(verbatim: format.format(input))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
extension TextField where Label == Text {
|
|
56
|
+
/// `TextField("Amount", value: $amount, format: .number)`: the field shows
|
|
57
|
+
/// the formatted value and sets it from what the user types when the
|
|
58
|
+
/// style's parse strategy accepts the text.
|
|
59
|
+
public init<S: StringProtocol, F: ParseableFormatStyle>(_ title: S, value: Binding<F.FormatInput>, format: F, prompt: Text? = nil)
|
|
60
|
+
where F.FormatOutput == String, F.Strategy.ParseInput == String {
|
|
61
|
+
self.init(_placeholder: prompt?._content ?? String(title), value: value, format: _ParseableFormatStyleText(style: format))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public init<F: ParseableFormatStyle>(_ titleKey: LocalizedStringKey, value: Binding<F.FormatInput>, format: F, prompt: Text? = nil)
|
|
65
|
+
where F.FormatOutput == String, F.Strategy.ParseInput == String {
|
|
66
|
+
self.init(_placeholder: prompt?._content ?? titleKey.key, value: value, format: _ParseableFormatStyleText(style: format))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// MARK: - Defaults and resources
|
|
71
|
+
|
|
72
|
+
/// `UserDefaults` that persists through the browser and the `Bundle` that
|
|
73
|
+
/// reads the app's resources (Sources/_FoundationCommon). The CLI's generated
|
|
74
|
+
/// file aliases both in the app, over corelibs Foundation's (whose
|
|
75
|
+
/// `UserDefaults` cannot persist on WASI and whose `Bundle.main` traps).
|
|
76
|
+
public typealias UserDefaults = _FoundationCommon.UserDefaults
|
|
77
|
+
public typealias Bundle = _FoundationCommon.Bundle
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// `Locale.current` for the browser. swift-foundation has no way to set the
|
|
2
|
+
// current locale on WASI: `LocaleCache.preferences()` returns a hard-coded
|
|
3
|
+
// `en_001` (there are no CFPreferences and it does not read `LANG`), so a
|
|
4
|
+
// French user would get English formatting whatever the browser says.
|
|
5
|
+
//
|
|
6
|
+
// The CLI links full-Foundation apps with
|
|
7
|
+
// `--wrap=$s20FoundationEssentials11LocaleCacheV11preferencesAA0C11PreferencesV_SbtyF`
|
|
8
|
+
// (and the same for `Locale.preferredLanguages`), so the one call to that
|
|
9
|
+
// function, from `_currentAndCache`, lands here instead and reads the locale
|
|
10
|
+
// the host chose (`SB_LOCALE`, a BCP 47 tag among the app's shipped locales).
|
|
11
|
+
//
|
|
12
|
+
// This depends on the SDK's swift-foundation: the struct below mirrors
|
|
13
|
+
// `LocalePreferences` field for field (the non-framework build at Swift 6.4,
|
|
14
|
+
// 60 bytes on wasm32), and the method is on a class so the cache pointer rides
|
|
15
|
+
// in Swift's `self` slot like the original's. The SDK is pinned by the CLI
|
|
16
|
+
// (`SWIFT_VERSION`), and CI checks French formatting in a full-mode build, so
|
|
17
|
+
// a layout change in a future SDK shows up as a failed check, not a silent one.
|
|
18
|
+
// See docs/ops-protocol.md, "Phase 11".
|
|
19
|
+
|
|
20
|
+
#if os(WASI)
|
|
21
|
+
import Foundation
|
|
22
|
+
|
|
23
|
+
/// Mirror of `FoundationEssentials.LocalePreferences` (`package struct`, non-framework fields).
|
|
24
|
+
public struct _LocalePreferencesMirror {
|
|
25
|
+
public enum MeasurementUnit: Int { case centimeters, inches }
|
|
26
|
+
public enum TemperatureUnit: Int { case fahrenheit, celsius }
|
|
27
|
+
public enum CalendarIdentifier: Int { case gregorian }
|
|
28
|
+
public var metricUnits: Bool?
|
|
29
|
+
public var languages: [String]?
|
|
30
|
+
public var locale: String?
|
|
31
|
+
public var collationOrder: String?
|
|
32
|
+
public var firstWeekday: [CalendarIdentifier: Int]?
|
|
33
|
+
public var minDaysInFirstWeek: [CalendarIdentifier: Int]?
|
|
34
|
+
public var numberSymbols: [UInt32: String]?
|
|
35
|
+
public var country: String?
|
|
36
|
+
public var measurementUnits: MeasurementUnit?
|
|
37
|
+
public var temperatureUnit: TemperatureUnit?
|
|
38
|
+
public var force24Hour: Bool?
|
|
39
|
+
public var force12Hour: Bool?
|
|
40
|
+
public init() {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// The locale the host asked for, as ICU (`fr_FR`) and BCP 47 (`fr-FR`) identifiers; nil when unset.
|
|
44
|
+
func _requestedLocale() -> (icu: String, bcp47: String)? {
|
|
45
|
+
guard let raw = ProcessInfo.processInfo.environment["SB_LOCALE"]?.trimmingCharacters(in: .whitespaces), !raw.isEmpty else { return nil }
|
|
46
|
+
let parts = raw.split(whereSeparator: { $0 == "-" || $0 == "_" }).map(String.init)
|
|
47
|
+
guard let language = parts.first, (2...3).contains(language.count), language.allSatisfy(\.isLetter) else { return nil }
|
|
48
|
+
var icu = [language.lowercased()]
|
|
49
|
+
var bcp = [language.lowercased()]
|
|
50
|
+
for part in parts.dropFirst() {
|
|
51
|
+
if part.count == 4, part.allSatisfy(\.isLetter) {
|
|
52
|
+
let script = part.prefix(1).uppercased() + part.dropFirst().lowercased()
|
|
53
|
+
icu.append(script); bcp.append(script)
|
|
54
|
+
} else if part.count == 2 && part.allSatisfy(\.isLetter) || part.count == 3 && part.allSatisfy(\.isNumber) {
|
|
55
|
+
icu.append(part.uppercased()); bcp.append(part.uppercased())
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return (icu.joined(separator: "_"), bcp.joined(separator: "-"))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Stands in for the `LocaleCache` instance: Swift passes `self` in its own
|
|
62
|
+
/// slot (an explicit trailing parameter on wasm) and a class method is what
|
|
63
|
+
/// puts it there. `self` is the cache's storage and is never touched.
|
|
64
|
+
public final class _LocaleCacheHandle {
|
|
65
|
+
@_silgen_name("__wrap_$s20FoundationEssentials11LocaleCacheV11preferencesAA0C11PreferencesV_SbtyF")
|
|
66
|
+
public func _preferences() -> (_LocalePreferencesMirror, Bool) {
|
|
67
|
+
var prefs = _LocalePreferencesMirror()
|
|
68
|
+
if let locale = _requestedLocale() {
|
|
69
|
+
prefs.locale = locale.icu
|
|
70
|
+
prefs.languages = [locale.bcp47]
|
|
71
|
+
} else {
|
|
72
|
+
prefs.locale = "en_001"
|
|
73
|
+
prefs.languages = ["en-001"]
|
|
74
|
+
}
|
|
75
|
+
return (prefs, true)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Stands in for the `Locale` metatype of the static `preferredLanguages` getter.
|
|
80
|
+
public final class _LocaleMetatypeHandle {
|
|
81
|
+
@_silgen_name("__wrap_$s20FoundationEssentials6LocaleV18preferredLanguagesSaySSGvgZ")
|
|
82
|
+
public func _preferredLanguages() -> [String] {
|
|
83
|
+
[_requestedLocale()?.bcp47 ?? "en-001"]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
#endif
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// `Timer` and `RunLoop` for full-Foundation apps in the browser. swift-corelibs
|
|
2
|
+
// Foundation marks `RunLoop` unavailable on WASI and its `Timer` needs
|
|
3
|
+
// CFRunLoop, which the WASI build does not have (the link fails), and
|
|
4
|
+
// `Timer.publish` is Combine's anyway. These two classes have the same names;
|
|
5
|
+
// the CLI's staging gives the app `typealias Timer = FoundationBridge.Timer`
|
|
6
|
+
// and `typealias RunLoop = FoundationBridge.RunLoop`, so `Timer.publish(every:
|
|
7
|
+
// 1, on: .main, in: .common).autoconnect()` and `Timer.scheduledTimer` resolve
|
|
8
|
+
// here. Ticks come from the runtime's executor (`_IntervalPublisher` and
|
|
9
|
+
// `_ExecutorTimer` in the SwiftUI shim), like FoundationLite's.
|
|
10
|
+
|
|
11
|
+
import Foundation
|
|
12
|
+
import SwiftUI
|
|
13
|
+
|
|
14
|
+
public final class RunLoop: @unchecked Sendable {
|
|
15
|
+
public struct Mode: Hashable, Sendable {
|
|
16
|
+
public let rawValue: String
|
|
17
|
+
public init(rawValue: String) { self.rawValue = rawValue }
|
|
18
|
+
public static let `default` = Mode(rawValue: "default")
|
|
19
|
+
public static let common = Mode(rawValue: "common")
|
|
20
|
+
}
|
|
21
|
+
nonisolated(unsafe) public static let main = RunLoop()
|
|
22
|
+
public static var current: RunLoop { main }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public final class Timer: @unchecked Sendable {
|
|
26
|
+
/// Emits the current date every `interval` seconds once subscribed.
|
|
27
|
+
public typealias TimerPublisher = _IntervalPublisher<Date>
|
|
28
|
+
|
|
29
|
+
public static func publish(every interval: TimeInterval, tolerance: TimeInterval? = nil, on runLoop: RunLoop = .main, in mode: RunLoop.Mode = .default, options: Never? = nil) -> TimerPublisher {
|
|
30
|
+
_IntervalPublisher(every: interval) { Date() }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private var core: _ExecutorTimer!
|
|
34
|
+
|
|
35
|
+
public var isValid: Bool { core.isValid }
|
|
36
|
+
public var timeInterval: TimeInterval { core.timeInterval }
|
|
37
|
+
|
|
38
|
+
init(interval: TimeInterval, repeats: Bool, block: @escaping @Sendable (Timer) -> Void) {
|
|
39
|
+
core = _ExecutorTimer(interval: interval, repeats: repeats) { _ in block(self) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@discardableResult
|
|
43
|
+
public static func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping @Sendable (Timer) -> Void) -> Timer {
|
|
44
|
+
Timer(interval: interval, repeats: repeats, block: block)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public func invalidate() { core.invalidate() }
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// A stand-in for UIKit's `UITextChecker`, which SwiftUI apps reach through
|
|
2
|
+
// SwiftUI's re-export of UIKit (Hacking with SwiftUI's WordScramble checks
|
|
3
|
+
// spelling with it). Full-Foundation builds only (NSRange is Foundation's). The
|
|
4
|
+
// browser has no spell checker to ask, so every word
|
|
5
|
+
// passes: `rangeOfMisspelledWord` is always `NSNotFound`, `guesses` and
|
|
6
|
+
// `completions` are nil. Enough for the code to compile and run; the game
|
|
7
|
+
// just stops rejecting misspellings.
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
|
|
11
|
+
public final class UITextChecker: @unchecked Sendable {
|
|
12
|
+
public init() {}
|
|
13
|
+
|
|
14
|
+
/// Always "no misspelling": `NSRange(location: NSNotFound, length: 0)`.
|
|
15
|
+
public func rangeOfMisspelledWord(in stringToCheck: String, range: NSRange, startingAt startingOffset: Int,
|
|
16
|
+
wrap wrapFlag: Bool, language: String) -> NSRange {
|
|
17
|
+
NSRange(location: NSNotFound, length: 0)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public func guesses(forWordRange range: NSRange, in string: String, language: String) -> [String]? { nil }
|
|
21
|
+
public func completions(forPartialWordRange range: NSRange, in string: String, language: String) -> [String]? { nil }
|
|
22
|
+
|
|
23
|
+
public static let availableLanguages: [String] = ["en"]
|
|
24
|
+
public static func hasLearnedWord(_ word: String) -> Bool { false }
|
|
25
|
+
public static func learnWord(_ word: String) {}
|
|
26
|
+
public static func unlearnWord(_ word: String) {}
|
|
27
|
+
public func ignoreWord(_ wordToIgnore: String) {}
|
|
28
|
+
public func ignoredWords() -> [String]? { nil }
|
|
29
|
+
public func setIgnoredWords(_ words: [String]?) {}
|
|
30
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/// The slice of Foundation's formatting API that SwiftUI apps lean on, for
|
|
2
|
+
/// platforms where only `FoundationEssentials` is linked (WebAssembly, Linux):
|
|
3
|
+
/// the CLI's lean mode and the repository's own examples.
|
|
4
|
+
///
|
|
5
|
+
/// Full Foundation on Wasm costs about 14 MB more than FoundationEssentials
|
|
6
|
+
/// even with its ICU data trimmed (docs/ops-protocol.md, "Phase 11"), so apps
|
|
7
|
+
/// that must stay small import `FoundationEssentials` plus this module; the
|
|
8
|
+
/// CLI's default is the real Foundation with `FoundationBridge`.
|
|
9
|
+
/// The spellings match Foundation's, so a file that does
|
|
10
|
+
///
|
|
11
|
+
/// #if canImport(FoundationEssentials)
|
|
12
|
+
/// import FoundationEssentials
|
|
13
|
+
/// import FoundationLite
|
|
14
|
+
/// #else
|
|
15
|
+
/// import Foundation
|
|
16
|
+
/// #endif
|
|
17
|
+
///
|
|
18
|
+
/// compiles unchanged against Apple's SDKs. Output is fixed to US English
|
|
19
|
+
/// conventions (`1,234.5`, `Nov 14, 2023`, `10:13 PM`), which is also what
|
|
20
|
+
/// keeps headless snapshots byte-stable.
|
|
21
|
+
#if canImport(FoundationEssentials)
|
|
22
|
+
import FoundationEssentials
|
|
23
|
+
|
|
24
|
+
// MARK: - Dates
|
|
25
|
+
|
|
26
|
+
extension Date {
|
|
27
|
+
/// Namespaces for `formatted(date:time:)`, mirroring `Foundation.Date.FormatStyle`.
|
|
28
|
+
public struct FormatStyle: Sendable {
|
|
29
|
+
public enum DateStyle: Hashable, Sendable {
|
|
30
|
+
case omitted, numeric, abbreviated, long, complete
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public enum TimeStyle: Hashable, Sendable {
|
|
34
|
+
case omitted, shortened, standard, complete
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// `11/14/2023, 10:13 PM`, `Nov 14, 2023`, `November 14, 2023`,
|
|
39
|
+
/// `Tuesday, November 14, 2023`; times `10:13 PM`, `10:13:20 PM`.
|
|
40
|
+
/// Uses `Calendar.current`, which is Gregorian/UTC on WebAssembly.
|
|
41
|
+
public func formatted(date dateStyle: FormatStyle.DateStyle = .numeric,
|
|
42
|
+
time timeStyle: FormatStyle.TimeStyle = .shortened) -> String {
|
|
43
|
+
let calendar = Calendar.current
|
|
44
|
+
let c = calendar.dateComponents([.year, .month, .day, .weekday, .hour, .minute, .second], from: self)
|
|
45
|
+
let year = c.year ?? 1970, month = c.month ?? 1, day = c.day ?? 1
|
|
46
|
+
let monthName = _monthNames[(month - 1) % 12]
|
|
47
|
+
let datePart: String?
|
|
48
|
+
switch dateStyle {
|
|
49
|
+
case .omitted: datePart = nil
|
|
50
|
+
case .numeric: datePart = "\(month)/\(day)/\(year)"
|
|
51
|
+
case .abbreviated: datePart = "\(monthName.prefix(3)) \(day), \(year)"
|
|
52
|
+
case .long: datePart = "\(monthName) \(day), \(year)"
|
|
53
|
+
case .complete: datePart = "\(_weekdayNames[((c.weekday ?? 1) - 1) % 7]), \(monthName) \(day), \(year)"
|
|
54
|
+
}
|
|
55
|
+
let timePart: String?
|
|
56
|
+
if timeStyle == .omitted {
|
|
57
|
+
timePart = nil
|
|
58
|
+
} else {
|
|
59
|
+
let hour24 = c.hour ?? 0, minute = c.minute ?? 0, second = c.second ?? 0
|
|
60
|
+
let hour12 = hour24 % 12 == 0 ? 12 : hour24 % 12
|
|
61
|
+
let suffix = hour24 < 12 ? "AM" : "PM"
|
|
62
|
+
let base = "\(hour12):\(_twoDigits(minute))"
|
|
63
|
+
switch timeStyle {
|
|
64
|
+
case .shortened: timePart = "\(base) \(suffix)"
|
|
65
|
+
case .standard, .complete: timePart = "\(base):\(_twoDigits(second)) \(suffix)"
|
|
66
|
+
case .omitted: timePart = nil
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
switch (datePart, timePart) {
|
|
70
|
+
case (let d?, let t?): return "\(d), \(t)"
|
|
71
|
+
case (let d?, nil): return d
|
|
72
|
+
case (nil, let t?): return t
|
|
73
|
+
case (nil, nil): return ""
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public func formatted() -> String { formatted(date: .numeric, time: .shortened) }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private let _monthNames = ["January", "February", "March", "April", "May", "June", "July",
|
|
81
|
+
"August", "September", "October", "November", "December"]
|
|
82
|
+
private let _weekdayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
|
83
|
+
|
|
84
|
+
func _twoDigits(_ value: Int) -> String { value < 10 ? "0\(value)" : "\(value)" }
|
|
85
|
+
|
|
86
|
+
// MARK: - Numbers
|
|
87
|
+
|
|
88
|
+
/// Inserts thousands separators into a run of digits (optionally signed).
|
|
89
|
+
func _grouped(_ digits: Substring) -> String {
|
|
90
|
+
var sign = ""
|
|
91
|
+
var body = digits
|
|
92
|
+
if body.first == "-" {
|
|
93
|
+
sign = "-"
|
|
94
|
+
body = body.dropFirst()
|
|
95
|
+
}
|
|
96
|
+
var out: [Character] = []
|
|
97
|
+
for (index, ch) in body.reversed().enumerated() {
|
|
98
|
+
if index > 0 && index % 3 == 0 { out.append(",") }
|
|
99
|
+
out.append(ch)
|
|
100
|
+
}
|
|
101
|
+
return sign + String(out.reversed())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Formats a decimal string (`-1234.5000`) the way `.number` does: grouped
|
|
105
|
+
/// integer digits and at most `maxFractionDigits` fraction digits, trailing
|
|
106
|
+
/// zeros dropped.
|
|
107
|
+
func _formatDecimalString(_ text: String, maxFractionDigits: Int = 3) -> String {
|
|
108
|
+
let parts = text.split(separator: ".", maxSplits: 1, omittingEmptySubsequences: false)
|
|
109
|
+
let integer = _grouped(parts[0])
|
|
110
|
+
guard parts.count == 2 else { return integer }
|
|
111
|
+
var fraction = String(parts[1].prefix(maxFractionDigits))
|
|
112
|
+
while fraction.last == "0" { fraction.removeLast() }
|
|
113
|
+
return fraction.isEmpty ? integer : "\(integer).\(fraction)"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
extension BinaryInteger {
|
|
117
|
+
/// `1234` → `1,234`.
|
|
118
|
+
public func formatted() -> String { _grouped(Substring(String(self))) }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
extension BinaryFloatingPoint where Self: CustomStringConvertible {
|
|
122
|
+
/// `1234.5678` → `1,234.568`; whole numbers lose the `.0`.
|
|
123
|
+
public func formatted() -> String {
|
|
124
|
+
guard isFinite else { return isNaN ? "NaN" : (self < 0 ? "-∞" : "∞") }
|
|
125
|
+
// Round to 3 fraction digits in integer arithmetic to avoid exponents.
|
|
126
|
+
let scaled = (Double(self) * 1000).rounded()
|
|
127
|
+
let negative = scaled < 0
|
|
128
|
+
let magnitude = Int64(min(abs(scaled), Double(Int64.max / 2)))
|
|
129
|
+
let integer = magnitude / 1000
|
|
130
|
+
let fraction = magnitude % 1000
|
|
131
|
+
let text = "\(negative && magnitude != 0 ? "-" : "")\(integer)." + _threeDigits(fraction)
|
|
132
|
+
return _formatDecimalString(text)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Three-digit zero-padded fraction.
|
|
137
|
+
private func _threeDigits(_ value: Int64) -> String {
|
|
138
|
+
let raw = String(value)
|
|
139
|
+
return String(repeating: "0", count: max(0, 3 - raw.count)) + raw
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
extension Decimal {
|
|
143
|
+
/// `Decimal(string: "3.75")! * 4` → `15`; `Decimal(string: "1234.5")!` → `1,234.5`.
|
|
144
|
+
public func formatted() -> String { _formatDecimalString(description) }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// MARK: - Strings
|
|
148
|
+
|
|
149
|
+
extension StringProtocol {
|
|
150
|
+
public func localizedCaseInsensitiveContains<T: StringProtocol>(_ other: T) -> Bool {
|
|
151
|
+
lowercased().contains(other.lowercased())
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public func localizedStandardContains<T: StringProtocol>(_ other: T) -> Bool {
|
|
155
|
+
localizedCaseInsensitiveContains(other)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public func localizedCompare<T: StringProtocol>(_ other: T) -> ComparisonResult {
|
|
159
|
+
let a = String(self), b = String(other)
|
|
160
|
+
return a == b ? .orderedSame : (a < b ? .orderedAscending : .orderedDescending)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public func localizedCaseInsensitiveCompare<T: StringProtocol>(_ other: T) -> ComparisonResult {
|
|
164
|
+
lowercased().localizedCompare(other.lowercased())
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public func localizedStandardCompare<T: StringProtocol>(_ other: T) -> ComparisonResult {
|
|
168
|
+
localizedCaseInsensitiveCompare(other)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
#endif
|