Как заставить ошибки говорить на языке пользователя?
Разработчики Swift часто сталкиваются с проблемой: ошибка выброшена, но пользователь видит нечто вроде Error Domain=MyApp Code=42. Это технически верно, но абсолютно бесполезно для человека. На Stack Overflow вопрос "How to provide a localized description with an Error type in Swift?" собрал тысячи просмотров — и неспроста.
В этой статье мы разберём, как превратить сухие коды ошибок в понятные, локализованные сообщения. Вы узнаете, как использовать протокол LocalizedError, работать с NSError и правильно подготавливать строки для разных языков. Готовы сделать ваше приложение дружелюбнее? Поехали!
Почему просто Error не работает?
В Swift протокол Error — это минимальный контракт. Он не требует ни описания, ни локализации. Когда вы создаёте свою ошибку:
enum NetworkError: Error {
case timeout
case serverError(code: Int)
}
...вы получаете лишь имя кейса. Попробуйте вызвать localizedDescription:
let error = NetworkError.timeout
print(error.localizedDescription) // "The operation couldn’t be completed. (NetworkError error 0.)"
Система подставляет стандартную фразу — неинформативно и некрасиво. Чтобы дать пользователю осмысленное сообщение, нужно реализовать протокол LocalizedError.
Знакомьтесь: протокол LocalizedError
LocalizedError расширяет Error и добавляет четыре опциональных свойства:
errorDescription— основное сообщение для пользователяfailureReason— причина ошибки (техническая)recoverySuggestion— что делать пользователюhelpAnchor— ссылка на справку (редко используется)
Самое важное — errorDescription. Именно его отображают системные компоненты (алерты, SwiftUI).
Реализуем локализованную ошибку
Добавим LocalizedError к нашему NetworkError:
enum NetworkError: LocalizedError {
case timeout
case serverError(code: Int)
case noInternet
var errorDescription: String? {
switch self {
case .timeout:
return NSLocalizedString(
"Сервер не отвечает. Проверьте соединение и попробуйте снова.",
comment: "Timeout error"
)
case .serverError(let code):
let format = NSLocalizedString(
"Ошибка сервера: %d. Попробуйте позже.",
comment: "Server error with code"
)
return String(format: format, code)
case .noInternet:
return NSLocalizedString(
"Нет подключения к интернету.",
comment: "No internet error"
)
}
}
var failureReason: String? {
switch self {
case .timeout:
return NSLocalizedString(
"Превышено время ожидания ответа от сервера.",
comment: "Timeout reason"
)
case .serverError(let code):
return NSLocalizedString(
"Сервер вернул код \(code).",
comment: "Server error reason"
)
case .noInternet:
return NSLocalizedString(
"Отсутствует активное интернет-соединение.",
comment: "No internet reason"
)
}
}
var recoverySuggestion: String? {
switch self {
case .timeout:
return NSLocalizedString(
"Проверьте Wi-Fi или мобильные данные и повторите запрос.",
comment: "Timeout recovery"
)
case .serverError:
return NSLocalizedString(
"Подождите несколько минут и попробуйте снова.",
comment: "Server error recovery"
)
case .noInternet:
return NSLocalizedString(
"Включите интернет и повторите попытку.",
comment: "No internet recovery"
)
}
}
}
Теперь error.localizedDescription вернёт осмысленную строку на языке пользователя:
let error = NetworkError.timeout
print(error.localizedDescription)
// "Сервер не отвечает. Проверьте соединение и попробуйте снова."
Работа с NSError: мост между мирами
Иногда вам нужно взаимодействовать с Objective-C кодом или системными API, которые используют NSError. Swift автоматически мостит Error в NSError, но локализованное описание теряется, если не реализовать LocalizedError.
Хорошая новость: если вы реализовали errorDescription, то NSError автоматически подхватит его через userInfo[NSLocalizedDescriptionKey].
Пример работы с системным API:
func loadData() throws {
// Представим, что возникла сетевая ошибка
throw NetworkError.timeout
}
do {
try loadData()
} catch {
let nsError = error as NSError
print(nsError.localizedDescription) // Ваше описание!
print(nsError.userInfo[NSLocalizedFailureReasonErrorKey] ?? "")
print(nsError.userInfo[NSLocalizedRecoverySuggestionErrorKey] ?? "")
}
Локализация строк: NSLocalizedString и String Catalog
Для настоящей локализации используйте NSLocalizedString. Храните строки в Localizable.strings (Xcode 15+ рекомендует String Catalog).
Пример Localizable.strings для английского:
"Сервер не отвечает. Проверьте соединение и попробуйте снова." = "Server is not responding. Check your connection and try again.";
"Ошибка сервера: %d. Попробуйте позже." = "Server error: %d. Please try later.";
"Нет подключения к интернету." = "No internet connection.";
Для других языков создайте соответствующие файлы (Localizable.strings (Russian) — для русского, Localizable.strings (English) — для английского).
Продвинутый пример: структура с параметрами
Иногда ошибка содержит динамические данные — например, имя файла или ID пользователя. Создадим структуру:
struct FileError: LocalizedError {
let fileName: String
let operation: String
var errorDescription: String? {
let format = NSLocalizedString(
"Не удалось %@ файл \"%@\".",
comment: "File error description"
)
return String(format: format, operation, fileName)
}
var failureReason: String? {
let format = NSLocalizedString(
"Файл \"%@\" недоступен для %@.",
comment: "File error reason"
)
return String(format: format, fileName, operation)
}
var recoverySuggestion: String? {
NSLocalizedString(
"Проверьте права доступа и имя файла.",
comment: "File error recovery"
)
}
}
Использование:
let error = FileError(fileName: "data.txt", operation: "открыть")
print(error.localizedDescription)
// "Не удалось открыть файл "data.txt"."
Лучшие практики для production
- Всегда используйте
NSLocalizedString— даже если приложение только на русском. Это подготовит код к будущей локализации. - <