From 476b621522f79a27d7a032aae4e0a3bbb8f5b0a7 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 30 May 2026 17:28:46 +0800 Subject: [PATCH 1/3] Implement EnvironmentConfigurableFormatter --- .../EnvironmentConfigurableFormatter.swift | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift diff --git a/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift b/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift new file mode 100644 index 000000000..f8b64f2c2 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift @@ -0,0 +1,91 @@ +// +// EnvironmentConfigurableFormatter.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +public import Foundation + +// MARK: - EnvironmentConfigurableFormatter + +protocol EnvironmentConfigurableFormatter: AnyObject { + func configure(in environment: EnvironmentValues) +} + +// MARK: - DateFormatter + EnvironmentConfigurableFormatter + +extension DateFormatter: EnvironmentConfigurableFormatter { + func configure(in environment: EnvironmentValues) { + locale = environment.locale + calendar = environment.calendar + timeZone = environment.timeZone + } +} + +// MARK: - ISO8601DateFormatter + EnvironmentConfigurableFormatter + +extension ISO8601DateFormatter: EnvironmentConfigurableFormatter { + func configure(in environment: EnvironmentValues) { + timeZone = environment.timeZone + } +} + +// MARK: - DateComponentsFormatter + EnvironmentConfigurableFormatter + +extension DateComponentsFormatter: EnvironmentConfigurableFormatter { + func configure(in environment: EnvironmentValues) { + calendar = environment.calendar + } +} + +// MARK: - DateIntervalFormatter + EnvironmentConfigurableFormatter + +extension DateIntervalFormatter: EnvironmentConfigurableFormatter { + func configure(in environment: EnvironmentValues) { + calendar = environment.calendar + locale = environment.locale + timeZone = environment.timeZone + } +} + +// MARK: - NumberFormatter + EnvironmentConfigurableFormatter + +extension NumberFormatter: EnvironmentConfigurableFormatter { + func configure(in environment: EnvironmentValues) { + locale = environment.locale + } +} + +// MARK: - MeasurementFormatter + EnvironmentConfigurableFormatter + +extension MeasurementFormatter: EnvironmentConfigurableFormatter { + func configure(in environment: EnvironmentValues) { + locale = environment.locale + numberFormatter.locale = environment.locale + } +} + +// MARK: - MassFormatter + EnvironmentConfigurableFormatter + +extension MassFormatter: EnvironmentConfigurableFormatter { + func configure(in environment: EnvironmentValues) { + numberFormatter.locale = environment.locale + } +} + +// MARK: - LengthFormatter + EnvironmentConfigurableFormatter + +extension LengthFormatter: EnvironmentConfigurableFormatter { + func configure(in environment: EnvironmentValues) { + numberFormatter.locale = environment.locale + } +} + +// MARK: - EnergyFormatter + EnvironmentConfigurableFormatter + +extension EnergyFormatter: EnvironmentConfigurableFormatter { + func configure(in environment: EnvironmentValues) { + numberFormatter.locale = environment.locale + } +} From 2d854acab13e814710b41c61f2999f3560ce3c13 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 30 May 2026 18:03:38 +0800 Subject: [PATCH 2/3] Guard unavailable formatter conformances --- .../View/Text/Util/EnvironmentConfigurableFormatter.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift b/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift index f8b64f2c2..9bfbc7d1a 100644 --- a/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift +++ b/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift @@ -33,11 +33,13 @@ extension ISO8601DateFormatter: EnvironmentConfigurableFormatter { // MARK: - DateComponentsFormatter + EnvironmentConfigurableFormatter +#if canImport(Darwin) extension DateComponentsFormatter: EnvironmentConfigurableFormatter { func configure(in environment: EnvironmentValues) { calendar = environment.calendar } } +#endif // MARK: - DateIntervalFormatter + EnvironmentConfigurableFormatter @@ -59,12 +61,14 @@ extension NumberFormatter: EnvironmentConfigurableFormatter { // MARK: - MeasurementFormatter + EnvironmentConfigurableFormatter +#if canImport(Darwin) extension MeasurementFormatter: EnvironmentConfigurableFormatter { func configure(in environment: EnvironmentValues) { locale = environment.locale numberFormatter.locale = environment.locale } } +#endif // MARK: - MassFormatter + EnvironmentConfigurableFormatter From 1520e5fc491a4c9207f124cd97e0691558193d69 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 30 May 2026 18:05:13 +0800 Subject: [PATCH 3/3] Document and test environment configurable formatters --- .../EnvironmentConfigurableFormatter.swift | 13 ++ ...nvironmentConfigurableFormatterTests.swift | 112 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 Tests/OpenSwiftUICoreTests/View/Text/Util/EnvironmentConfigurableFormatterTests.swift diff --git a/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift b/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift index 9bfbc7d1a..564f6110b 100644 --- a/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift +++ b/Sources/OpenSwiftUICore/View/Text/Util/EnvironmentConfigurableFormatter.swift @@ -9,7 +9,20 @@ public import Foundation // MARK: - EnvironmentConfigurableFormatter +/// A formatter that can apply text-related environment values before use. +/// +/// OpenSwiftUI calls this hook for Foundation formatters whose output depends +/// on environment-provided formatting state, such as locale, calendar, or time +/// zone. Conforming formatters update only the environment-dependent properties +/// they support. +/// +/// - Note: Some Foundation formatter types are only available on Apple +/// platforms. protocol EnvironmentConfigurableFormatter: AnyObject { + /// Updates the formatter with values from the current environment. + /// + /// - Parameter environment: The environment values that should influence + /// the formatter's output. func configure(in environment: EnvironmentValues) } diff --git a/Tests/OpenSwiftUICoreTests/View/Text/Util/EnvironmentConfigurableFormatterTests.swift b/Tests/OpenSwiftUICoreTests/View/Text/Util/EnvironmentConfigurableFormatterTests.swift new file mode 100644 index 000000000..ac5b9fbe7 --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/View/Text/Util/EnvironmentConfigurableFormatterTests.swift @@ -0,0 +1,112 @@ +// +// EnvironmentConfigurableFormatterTests.swift +// OpenSwiftUICoreTests + +import Foundation +@testable import OpenSwiftUICore +import Testing + +struct EnvironmentConfigurableFormatterTests { + @Test + func dateFormatterConfigureAppliesEnvironmentValues() { + let env = makeEnvironmentValues() + + let formatter = DateFormatter() + formatter.configure(in: env) + #expect(formatter.locale == env.locale) + #expect(formatter.calendar == env.calendar) + #expect(formatter.timeZone == env.timeZone) + } + + @Test + func iso8601DateFormatterConfigureAppliesEnvironmentValues() { + let env = makeEnvironmentValues() + + let formatter = ISO8601DateFormatter() + formatter.configure(in: env) + #expect(formatter.timeZone == env.timeZone) + } + + #if canImport(Darwin) + @Test + func dateComponentsFormatterConfigureAppliesEnvironmentValues() { + let env = makeEnvironmentValues() + + let formatter = DateComponentsFormatter() + formatter.configure(in: env) + #expect(formatter.calendar == env.calendar) + } + #endif + + @Test + func dateIntervalFormatterConfigureAppliesEnvironmentValues() { + let env = makeEnvironmentValues() + + let formatter = DateIntervalFormatter() + formatter.configure(in: env) + #expect(formatter.calendar == env.calendar) + #expect(formatter.locale == env.locale) + #expect(formatter.timeZone == env.timeZone) + } + + @Test + func numberFormatterConfigureAppliesEnvironmentValues() { + let env = makeEnvironmentValues() + + let formatter = NumberFormatter() + formatter.configure(in: env) + #expect(formatter.locale == env.locale) + } + + #if canImport(Darwin) + @Test + func measurementFormatterConfigureAppliesEnvironmentValues() { + let env = makeEnvironmentValues() + + let formatter = MeasurementFormatter() + formatter.configure(in: env) + #expect(formatter.locale == env.locale) + #expect(formatter.numberFormatter.locale == env.locale) + } + #endif + + @Test + func massFormatterConfigureAppliesEnvironmentValues() { + let env = makeEnvironmentValues() + + let formatter = MassFormatter() + formatter.configure(in: env) + #expect(formatter.numberFormatter.locale == env.locale) + } + + @Test + func lengthFormatterConfigureAppliesEnvironmentValues() { + let env = makeEnvironmentValues() + + let formatter = LengthFormatter() + formatter.configure(in: env) + #expect(formatter.numberFormatter.locale == env.locale) + } + + @Test + func energyFormatterConfigureAppliesEnvironmentValues() { + let env = makeEnvironmentValues() + + let formatter = EnergyFormatter() + formatter.configure(in: env) + #expect(formatter.numberFormatter.locale == env.locale) + } + + private func makeEnvironmentValues() -> EnvironmentValues { + let locale = Locale(identifier: "fr_FR") + var calendar = Calendar(identifier: .buddhist) + calendar.timeZone = TimeZone(secondsFromGMT: 9 * 3600)! + let timeZone = TimeZone(secondsFromGMT: 5 * 3600)! + + var environment = EnvironmentValues() + environment.locale = locale + environment.calendar = calendar + environment.timeZone = timeZone + return environment + } +}