From aa88a5b9261c4e692eddf17e16f19977c2aea5aa Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Mon, 19 Oct 2026 18:51:32 +0200 Subject: [PATCH] Build TimeZone.abbreviationDictionary on first use On Android a stored static's initializer runs when the class loads, so the walk over every known zone ran the first time anything touched TimeZone. UserDefaults does that during its own static init, putting the walk on the main thread at app launch. Build and cache the dictionary on first read instead; assigning it still replaces it. Co-Authored-By: Claude Opus 5.5 --- Sources/SkipFoundation/TimeZone.swift | 24 +++++++++++++++++-- .../DateTime/TestTimeZone.swift | 16 +++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Sources/SkipFoundation/TimeZone.swift b/Sources/SkipFoundation/TimeZone.swift index 7acebdd..c10bc65 100644 --- a/Sources/SkipFoundation/TimeZone.swift +++ b/Sources/SkipFoundation/TimeZone.swift @@ -131,7 +131,27 @@ public struct TimeZone : Hashable, Codable, CustomStringConvertible, Sendable, K return knownTimeZoneIdentifiers } - public static var abbreviationDictionary: [String : String] = { + // Built on first use: a stored static's initializer runs when the class loads on Android, and walking every + // zone through ICU there costs hundreds of milliseconds on low-end devices (UserDefaults touches TimeZone at launch). + public static var abbreviationDictionary: [String : String] { + get { + if let abbreviations = cachedAbbreviationDictionary { + return abbreviations + } + let abbreviations = buildAbbreviationDictionary() + cachedAbbreviationDictionary = abbreviations + return abbreviations + } + set { + cachedAbbreviationDictionary = newValue + } + } + + // Not synchronized: concurrent first reads may each build the same dictionary, which is harmless. + // Internal so tests can check it's built on demand. + internal static var cachedAbbreviationDictionary: [String : String]? = nil + + private static func buildAbbreviationDictionary() -> [String : String] { var abbreviations: [String : String] = [:] // Abbreviations are ambiguous. Keep the first identifier in sorted order so // the mapping is stable, and use English names regardless of the device locale. @@ -149,7 +169,7 @@ public struct TimeZone : Hashable, Codable, CustomStringConvertible, Sendable, K } } return abbreviations - }() + } @available(*, unavailable) public static var timeZoneDataVersion: String { diff --git a/Tests/SkipFoundationTests/DateTime/TestTimeZone.swift b/Tests/SkipFoundationTests/DateTime/TestTimeZone.swift index 6b7f60a..a031485 100644 --- a/Tests/SkipFoundationTests/DateTime/TestTimeZone.swift +++ b/Tests/SkipFoundationTests/DateTime/TestTimeZone.swift @@ -257,6 +257,22 @@ class TestTimeZone: XCTestCase { } } + func test_abbreviationDictionaryIsBuiltOnFirstUse() { + #if SKIP + // Building the dictionary walks every known zone through ICU, so plain TimeZone use must not trigger it + let original = TimeZone.cachedAbbreviationDictionary + defer { TimeZone.cachedAbbreviationDictionary = original } + TimeZone.cachedAbbreviationDictionary = nil + XCTAssertEqual(TimeZone(identifier: "Asia/Tokyo")?.identifier, "Asia/Tokyo") + XCTAssertNotNil(TimeZone(secondsFromGMT: 3600)) + let _ = TimeZone.current + XCTAssertNil(TimeZone.cachedAbbreviationDictionary) + + XCTAssertNotNil(TimeZone(abbreviation: "GMT")) + XCTAssertNotNil(TimeZone.cachedAbbreviationDictionary) + #endif + } + func test_initializingTimeZoneWithCustomAbbreviations() { let original = TimeZone.abbreviationDictionary defer { TimeZone.abbreviationDictionary = original }