2023-10-25 15:11:37 -07:00

135 lines
5.2 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#if FOUNDATION_FRAMEWORK
@_implementationOnly import _ForSwiftFoundation
import CoreFoundation
#endif
/// Singleton which listens for notifications about preference changes for Calendar and holds cached singletons for the current locale, calendar, and time zone.
struct CalendarCache : Sendable {
// MARK: - Concrete Classes
// _CalendarICU, if present
static var calendarICUClass: _CalendarProtocol.Type = {
#if FOUNDATION_FRAMEWORK && canImport(FoundationICU)
_CalendarICU.self
#else
if let name = _typeByName("FoundationInternationalization._CalendarICU"), let t = name as? _CalendarProtocol.Type {
return t
} else {
// Use the default gregorian class
return _CalendarGregorian.self
}
#endif
}()
// MARK: - State
struct State : Sendable {
// If nil, the calendar has been invalidated and will be created next time State.current() is called
private var currentCalendar: (any _CalendarProtocol)?
private var autoupdatingCurrentCalendar: _CalendarAutoupdating?
private var fixedCalendars: [Calendar.Identifier: any _CalendarProtocol] = [:]
private var noteCount = -1
private var wasResetManually = false
mutating func check() {
#if FOUNDATION_FRAMEWORK
// On Darwin we listen for certain distributed notifications to reset the current Calendar.
let newNoteCount = _CFLocaleGetNoteCount() + _CFTimeZoneGetNoteCount() + Int(_CFCalendarGetMidnightNoteCount())
#else
let newNoteCount = 1
#endif
if newNoteCount != noteCount || wasResetManually {
// rdar://102017659
// Don't create `currentCalendar` here to avoid deadlocking when retrieving a fixed
// calendar. Creating the current calendar gets the current locale, decodes a plist
// from CFPreferences, and may call +[NSDate initialize] on a separate thread. This
// leads to a deadlock if we are also initializing a class on the current thread
currentCalendar = nil
fixedCalendars = [:]
noteCount = newNoteCount
wasResetManually = false
}
}
mutating func current() -> any _CalendarProtocol {
check()
if let currentCalendar {
return currentCalendar
} else {
let id = Locale.current._calendarIdentifier
let calendar = CalendarCache.calendarICUClass.init(identifier: id, timeZone: nil, locale: Locale.current, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
currentCalendar = calendar
return calendar
}
}
mutating func autoupdatingCurrent() -> any _CalendarProtocol {
if let autoupdatingCurrentCalendar {
return autoupdatingCurrentCalendar
} else {
let calendar = _CalendarAutoupdating()
autoupdatingCurrentCalendar = calendar
return calendar
}
}
mutating func fixed(_ id: Calendar.Identifier) -> any _CalendarProtocol {
check()
if let cached = fixedCalendars[id] {
return cached
} else {
let new = CalendarCache.calendarICUClass.init(identifier: id, timeZone: nil, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
fixedCalendars[id] = new
return new
}
}
mutating func reset() {
wasResetManually = true
}
}
let lock: LockedState<State>
static let cache = CalendarCache()
fileprivate init() {
lock = LockedState(initialState: State())
}
func reset() {
lock.withLock { $0.reset() }
}
var current: any _CalendarProtocol {
lock.withLock { $0.current() }
}
var autoupdatingCurrent: any _CalendarProtocol {
lock.withLock { $0.autoupdatingCurrent() }
}
func fixed(_ id: Calendar.Identifier) -> any _CalendarProtocol {
lock.withLock { $0.fixed(id) }
}
func fixed(identifier: Calendar.Identifier, locale: Locale?, timeZone: TimeZone?, firstWeekday: Int?, minimumDaysInFirstWeek: Int?, gregorianStartDate: Date?) -> any _CalendarProtocol {
// Note: Only the ObjC NSCalendar initWithCoder supports gregorian start date values. For Swift it is always nil.
return CalendarCache.calendarICUClass.init(identifier: identifier, timeZone: timeZone, locale: locale, firstWeekday: firstWeekday, minimumDaysInFirstWeek: minimumDaysInFirstWeek, gregorianStartDate: gregorianStartDate)
}
}