TimeZone — е клас Foundation в iOS и macOS, който абстрахира информацията за часовите зони за коректно преобразуване на времето между географски региони. Според Apple Developer Documentation, 2024, TimeZone предоставя методи за работа с идентификатори на часови зони (IANA Time Zone Database), отклонения спрямо UTC и правила за преминуване към лятно часово време. Класът е интегриран с DateFormatter и Calendar, осигурявайки автоматичното прилагане на правилната часова зона при форматиране на дати. За разлика от ръчното изчисляване на отклонението, TimeZone автоматично актуализира данните при промяна на часовата зона на устройството.
Основни моменти
TimeZone — е стойностен тип в Swift, който предоставя информация за географска часова зона: отклонението спрямо UTC, името, абревиатурата и правилата за преминуване към лятно часово време. В Objective-C класът се нарича NSTimeZone. И двата класа се базират на IANA Time Zone Database (известна още като Olson база от данни), която съдържа историята на промените на часовите зони от 1970 г. насам.
Всяка инстанция на TimeZone съхранява идентификатор на часовата зона (напр. Europe/Moscow), текущото отклонение в секунди от UTC, флага isDaylightSavingTime и датата на следващото преминуване. Идентификаторът е първичен ключ: при инициализация TimeZone(identifier:), системата зарежда съответния запис от базата данни за часови зони на устройството.
Според данните на IANA (2024), базата данни съдържа над 600 уникални идентификатора на часови зони. Apple предоставя част от тази база в всяка версия на iOS и macOS, гарантирайки еднообразие на изчисленията на всички устройства без необходимост от мрежови заявки.
Архитектура — TimeZone в Foundation е изградена на двунивовна система: идентификатор на часовата зона (човешки четив наименование) и неговото числово представяне (отклонението от UTC). Системата автоматично избира текущата часова зона от настройките на устройството, но разработчикът може да ги предпраи за конкретни операции по форматиране.
TimeZone е тъсно свързан с Calendar и DateFormatter. При форматиране на дата, DateFormatter използва свойството timeZone на инстанция TimeZone, за да преобразува абсолютния момент от време (Date) в текстово представяне в съответната часова зона. Ако timeZone не е зададено, се използва системната часова зона по подразбиране — TimeZone.current.
| Тип | Инициализация | Характеристики |
|---|---|---|
| Текуща | TimeZone.current | Автоматично се актуализира при промяна на региона в настройките, следи лятното часово време |
| Фиксирана | TimeZone(identifier:) | Не зависи от региона на устройството. Прилага избрания идентификатор постоянно |
| UTC | TimeZone(secondsFromGMT: 0) | Часова зона без корекция. Идентификатор: GMT |
| С произволно отклонение | TimeZone(secondsFromGMT: 10800) | Фиксирано отклонение в секунди. Не отчита лятното часово време |
Важен нюанс: TimeZone(identifier:) връща nil за непознати идентификатори. Това е честа причина за крах на приложението — разработчиците забравят да обработят опционалната стойност, предавайки неправилен идентификатор от потребителския вход. За IANA идентификаторите, главните букви имат значение: Europe/Moscow — правилно, europe/moscow — nil.
IANA Time Zone Database използва формата „Регион/Град” (Continent/City), където регионът е един от континентите (Africa, America, Asia, Atlantic, Australia, Europe, Indian, Pacific) или океан, а градът е най-големото населено място в зоната на часовата зона. Този формат гарантира уникалност и четивост на идентификатора.
Освен основния формат, TimeZone поддържа три допълнителни метода за идентификация: абревиатури (MSK, EST, PST), трибуквени кодове на часови зони (GMT, UTC) и числови отклонения (+0300, -0500). Абревиатурите обаче са нееднозначни: EST може да означава като Eastern Standard Time (GMT-5), така и Eastern Summer Time (GMT+10) в Австралия. Apple препоръчва изключителното използване на IANA идентификатори.
import Foundation
// Получаване на всички познати идентификатори на часови зони
let allIdentifiers: [String] = TimeZone.knownTimeZoneIdentifiers
print("Общо часови зони: \(allIdentifiers.count)")
// Филтриране по регион
let europeZones = allIdentifiers.filter { $0.hasPrefix("Europe/") }
print("Европейски часови зони: \(europeZones)")
// Абревиатури (несе препоръчват за производство)
if let moscowTimeZone = TimeZone(abbreviation: "MSK") {
print("MSK секунди от GMT: \(moscowTimeZone.secondsFromGMT())")
}
// Намиране на идентификатор по отклонение
let utcPlus3 = TimeZone(secondsFromGMT: 10800)
print("Идентификатор: \(utcPlus3.identifier)")
Абревиатури в TimeZone.abbreviationDictionary съдържат абревиатури за всички познати часови зони, но този речник не гарантира уникалност: ключът PST може да съответства както на America/Los_Angeles, така и на Pacific/Pago_Pago. В производствен код, винаги използвайте IANA идентификатори.
TimeZone автоматично отчита преминуването към лятно и зимно часово време (DST — Daylight Saving Time) за всички региони, където се прилага. Системата използва исторически данни от IANA Time Zone Database, които включват точните дати на преминуване за всяка часова зона. Свойството isDaylightSavingTime връща true, ако часовата зона в момента се намира в лятно часово време.
Методът nextDaylightSavingTimeTransition позволява да разберете датата на следващото преминуване, което е полезно за планиране на бъдещи събития. Тази функционалност е особено важна за региони с чести промени на правилата DST, като Бразилия или Мароко — до 2024 г. Бразилия ежегодно променяше датите на преминуване, и ръчното изчисляване довеждаше до грешки в приложенията.
Според данните на Apple WWDC 2023, библиотеката ICU (International Components for Unicode), която е основата на Foundation, актуализира DST данните при всяка актуализация на iOS. Приложенията не трябва да кешират данни за лятното часово време повече от един ден след актуализация на системата — IANA базата може да се промени дори без актуализация на версията на ОС чрез корекции на часовите зони.
import Foundation
// Проверка на DST за Europe/Moscow
let moscow = TimeZone(identifier: "Europe/Moscow")!
let now = Date()
let isMoscowDST = moscow.isDaylightSavingTime(for: now)
print("Москва в момента е в DST: \(isMoscowDST)")
// Получаване на следващата дата на DST преход
if let nextTransition = moscow.nextDaylightSavingTimeTransition(
after: now
) {
let dstOffset = moscow.daylightSavingTimeOffset(
for: nextTransition
)
print("Следващ преход: \(nextTransition), DST отклонение: \(dstOffset)s")
}
// Безопасно преобразуване с осъзнаност за DST
let newYork = TimeZone(identifier: "America/New_York")!
let offsetNY = newYork.secondsFromGMT(for: now)
print("Текущо NY отклонение: \(offsetNY / 3600)h")
Критичен нюанс: secondsFromGMT(for:) отчита DST за посочената дата, докато secondsFromGMT() само за текущото време. При форматиране на исторически дати, винаги използвайте версията с параметър Date: secondsFromGMT(for: someHistoricalDate). Разликата може да достигне 1–2 часа, което е критично за журнали или исторически данни.
Форматиране на дата с конкретна часова зона — най-често срещаната задача при работа с TimeZone. DateFormatter използва свойството timeZone, за да преобразува Date в текст. Ако timeZone не е изрично зададено, форматърът използва TimeZone.current — часовата зона, зададена на устройството на потребителя, което може да доведе до неочаквани резултати за сървърни данни.
import Foundation
// Форматиране на дата в определена часова зона
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let tokyo = TimeZone(identifier: "Asia/Tokyo")!
formatter.timeZone = tokyo
let tokyoTime = formatter.string(from: Date())
print("Час в Токио: \(tokyoTime)")
// Налични идентификатори за избор от потребителя
let displayNames: [(String, String)] = TimeZone.knownTimeZoneIdentifiers
.prefix(20)
.map { ($0, TimeZone(identifier: $0)!.localizedName(
for: .generic, locale: .current
)) }
// Сравнение на две часови зони
let london = TimeZone(identifier: "Europe/London")!
let difference = tokyo.secondsFromGMT(for: Date())
- london.secondsFromGMT(for: Date())
print("Разлика Токио-Лондон: \(difference / 3600)h")
// Работа с речник на абревиатурите
let knownAbbrevs = TimeZone.abbreviationDictionary
for (abbr, ident) in knownAbbrevs.sorted(by: { $0.key < $1.key }).prefix(5) {
print("\(abbr) -> \(ident)")
}
Локализирано име на часовата зона чрез localizedName(for:locale:) връща човешки четиво име на посочения език. Например, за Europe/Moscow с руски локал, методът ще върне „Москва”, а с английски — „Moscow Time”. Наличните стилове са: .standard (стандартно име), .daylightSaving (лятно часово време) и .shortGeneric (късо).
import Foundation
let paris = TimeZone(identifier: "Europe/Paris")!
let nameRU = paris.localizedName(
for: .standard,
locale: Locale(identifier: "ru_RU")
)
print("Руско име: \(nameRU)")
// Проверка дали регионът е в същия ден
let isSameDay = Calendar.current.isDate(
Date(),
equalTo: Date(),
toGranularity: .day
)
print("Същият ден в различни часови зони: \(isSameDay)")
Сериализация на идентификатора на часовата зона — най-добрата практика за съхраняване на TimeZone в бази от данни или UserDefaults. Съхранявайте идентификатора (низ от тип Europe/Moscow), а не отклонението в секунди или абревиатурата. Отклонението може да се промени при промяна на DST, а абревиатурата е нееднозначна. Възстановяване: TimeZone(identifier: savedString).
Използване на фиксирано отклонение вместо идентификатор на часовата зона — най-често срещаната грешка. TimeZone(secondsFromGMT: 10800) не отчита DST, затова за Europe/Moscow през лятото тази конструкция дава грешно отклонение от 1 час. Винаги използвайте IANA идентификатор за региони с лятно часово време.
Забравена обработка на nil при инициализация TimeZone(identifier:) — втората най-често срещана грешка. Ако потребителят въведе идентификатор с грешка (напр. „moscow” вместо „Europe/Moscow”), конструкторът връща nil. Без обработка на опционалната стойност, приложението ще се срива с runtime error. Използвайте guard let или TimeZone(identifier:) с известен fallback.
Игнориране на DST при работа с бъдещи дати. TimeZone.secondsFromGMT(for:) — единственият правилен начин да получите отклонението за конкретна дата. Използването на secondsFromGMT() без параметър за исторически или бъдещи дати дава отклонението за текущия момент, което може да не съответства на действителното отклонение на посочената дата, особено за региони с отмяна или въвеждане на DST.
Според данните на Stack Overflow (2024), около 15% от въпросите, свързани с DateFormatter, са свързани с неправилно настрояване на timeZone. Типичен сценарий: сървърът изпраща дата в UTC, разработчикът го форматира без да зададе timeZone на форматъра, и датата се показва в часовата зона на устройството, което създава обръкване сред потребителите от различни региони. Правило: винаги изрично задавайте timeZone на форматъра за сървърни данни.
Често задавани въпроси
TimeZone — клас Foundation за работа с часови зони в iOS и macOS. Предоставя информация за отклонението спрямо UTC, правилата за преминуване към лятно часово време и идентификаторите на часовите зони на базата на IANA Time Zone Database.
Три формата: IANA идентификатори (Europe/Moscow), абревиатури (MSK, EST) и числови отклонения (+0300). Apple препоръчва използването на IANA идентификатори като единствен недвосмислен формат за производствен код.
Автоматично чрез методите secondsFromGMT(for:) и isDaylightSavingTime(for:). TimeZone използва исторически данни от IANA, които се актуализират при всяко издаване на iOS, гарантирайки правилни DST преходи за всяка дата.
TimeZone.current връща часовата зона, избрана от потребителя в настройките (може да се различава от географската). TimeZone.system връща часовата зона на устройството, която се определя автоматично на базата на геопозицията и не може да бъде предправена от потребителя.
TimeZone.current връща текущата часова зона на устройството. За да получите идентификатора, използвайте свойството identifier: TimeZone.current.identifier. За локализирано име, извикайте localizedName(for:locale:).
Заключение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също