Как автоматизировать безопасный декодинг массивов в Swift с @propertyWrapper

Привет! На связи Влад, iOS-разработчик из Ozon. Сегодня я поделюсь с вами, возможно, не самым очевидным способом использования propertyWrappers. Обёртки позволяют добавлять дополнительную логику свойствам. В одну из них мы спрятали описание безопасного декодинга массивов, и теперь нам достаточно пометить свойство как @SafeDecode — и всё начинает работает автоматически. О том, как они работают и как их завести у себя, читайте дальше.

Что такое безопасный декодинг

Для тех, кто сталкивается с безопасным декодингом впервые, поясню: безопасный декодинг массива — это декодинг, при котором в декодируемом массиве может содержаться элемент, не соответствующий ожидаемому формату; при этом в результате мы получим все элементы массива, которые смогли распарсить.

Например, у нас есть структура:

struct Article { let title: String // обязательное поле let subtitle: String? // не обязательное поле
}

И мы пытаемся распарсить такой массив данных:

[ { "title": "Title1", "subtitle": "Subtitle1" }, { // В этом элементе нет: "title": "Title1", "subtitle": "Subtitle1" }
]
do { let articles = try JSONDecoder().decode([Article].self, from: jsonData) } catch { print(error) // Мы получим ошибку: "No value associated with key title (\"title\")." // Потому что во втором элементе нет title, из-за этого // весь массив не распарсится
}

Чтобы всё-таки получить все остальные элементы, мы используем propertyWrapper. Он содержит внутри логику, которая фильтрует ошибки и возвращает полученные значения.

Для тех, кто ещё не работал с propertyWrapper

Если вы уже знаете, как работает обёртка свойств, смело переходите к следующему разделу. Или можете освежить знания.

PropertyWrapper — это обёртка, позволяющая добавлять дополнительную логику самому свойству. То есть, например, мы можем сделать так, чтобы все слова в строке начинались с заглавной буквы или чтобы числа в переменной были всегда меньше 12. И всё это — всего одной строкой.

Давайте попробуем.

Для начала сделаем основу propertyWrapper:

@propertyWrapper
struct Example { public var wrappedValue: Any public init(wrappedValue: Any) { self.wrappedValue = wrappedValue }
}

Она состоит из маркировки @propertyWrapper и обязательного свойства wrappedValue.

Эту обёртку уже можно использовать:

struct Numbers { @Example let value: Any
}

Но она пока что ничего не делает.

Посмотрим, как выглядит propertyWrapper, который будет устанавливать в свойство только положительные числа с помощью abs():

@propertyWrapper
struct Abs { private var number: Int = 0 var wrappedValue: Int { get { number } set { number = abs(newValue) } }
}

Вся логика работы у нас спрятана в одной строке: number = abs(newValue). Чтобы сделать из этой обёртки что-то новое, достаточно поменять только эту строку.

Также у нас нет init(wrappedValue: Any), как в основе, потому что мы сразу задали значение для number. Если этого не сделать, придётся дописать init().

Пример использования:

struct Number { @Abs var nonNegativeNumber: Int
} var number = Number()
number.nonNegativeNumber = -1
print(number.nonNegativeNumber) // 1
number.nonNegativeNumber = -77
print(number.nonNegativeNumber) // 77

Теперь любое число, установленное в nonNegativeNumber, будет положительным благодаря обёртке @Abs.

Давайте посмотрим, как ещё можно сделать эту же обёртку. Мы можем вместо приватного number сделать всё в wrappedValue, для этого нам понадобится наблюдатель свойства didSet {}:

@propertyWrapper
struct Abs { var wrappedValue: Int { didSet { wrappedValue = abs(wrappedValue) } } init(wrappedValue: Int) { self.wrappedValue = abs(wrappedValue) }
}

Результат будет тот же:

struct Number { @Abs var nonNegativeNumber: Int
} var number = Number()
number.nonNegativeNumber = -15
print(number.nonNegativeNumber) // 15
number.nonNegativeNumber = -40
print(number.nonNegativeNumber) // 40

А теперь рассмотрим пример, в котором propertyWrapper будет удалять цифры из конца строки:

@propertyWrapper
struct WithoutDecimalDigits { var wrappedValue: String { didSet { wrappedValue = wrappedValue.trimmingCharacters(in: .decimalDigits) } } init(wrappedValue: String) { self.wrappedValue = wrappedValue.trimmingCharacters(in: .decimalDigits) }
}

Вся логика работы содержится в didSet{}. При таком подходе нам обязательно нужно установить значение wrappedValue через init(). Это связано с тем, что наблюдатели свойств начинают работать только после установки значения в объект. Проще говоря, блок didSet{} заработает только после установки значения wrappedValue в init().

Реализация:

struct Example { @WithoutDecimalDigits var value: String
} let exampleString = Example(value: "Hello 123")
print(exampleString.value) // "Hello "

Теперь наша обёртка удаляет все цифры из строки.

Зная эти основы, можно делать удобные propertyWrappers для своего проекта. Но использовать их нужно с осторожностью. Если скрыть внутри сложную логику, то, в будущем, можно случайно добавить неочевидное поведение.

Как безопасно декодировать массив с propertyWrapper

Обёртки очень легко использовать:

struct Example: Decodable { @SafeArray let articlesArray: [Article]
}

Мы помечаем декодируемый массив как @SafeArray — и в нём будут все элементы, которые можно получить.

Чтобы propertyWrapper заработал, нужно сделать две вещи:

  1. Подготовить новый тип Throwable, который может содержать либо значение, либо ошибку.

  2. Написать расширение для SingleValueDecodingContainer.

Делаем тип, он будет очень простым:

enum Throwable<T: Decodable>: Decodable { case success(T) case failure(Error) init(from decoder: Decoder) throws { do { let decoded = try T(from: decoder) self = .success(decoded) } catch let error { self = .failure(error) } }
}

А теперь сделаем расширение.

Шаг 1. Подготавливаем расширение:

extension SingleValueDecodingContainer { func safelyDecodeArray<T>() throws -> [T] where T: Decodable { }
}

Шаг 2. Добавляем декодинг массива:

extension SingleValueDecodingContainer { func safelyDecodeArray<T>() throws -> [T] where T: Decodable { let decodedArray = (try? decode([Throwable<T>].self)) ?? [] }
}

Шаг 3. Фильтруем и возвращаем декодируемый массив:

extension SingleValueDecodingContainer { func safelyDecodeArray<T>() throws -> [T] where T: Decodable { let decodedArray = (try? decode([Throwable<T>].self)) ?? [] let filtredArray = decodedArray.compactMap { result -> T? in switch result { case let .success(value): return value case .failure(_): return nil } } return filtredArray }
}

В результате декодинга safelyDecodeArray вернёт либо все полученные элементы, либо пустой массив.

Следующие два шага — для тех, кто хочет добавить обработку ошибок и проверку на пустой массив; если вы хотите сразу перейти к реализации propertyWrapper, их можно пропустить.

Шаг 4. Добавляем проверку и возвращаем ошибку, если после фильтрации получился пустой массив:

extension SingleValueDecodingContainer { func safelyDecodeArray<T>() throws -> [T] where T: Decodable { ... if filtredArray.isEmpty { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Empty array of elements is not allowed") } return filtredArray }
}

Шаг 5. Добавляем возможность выводить все полученные ошибки через callback:

extension SingleValueDecodingContainer { // 1. Добавим callback для вывода описания ошибок onItemError: (([String: Any]) -> Void)? func safelyDecodeArray<T>(onItemError: (([String: Any]) -> Void)?) throws -> [T] where T: Decodable { let decodedArray = (try? decode([Throwable<T>].self)) ?? [] // 2. Чтобы иметь доступ к индексу элемента, добавим enumerated() и index let filtredArray = decodedArray.enumerated().compactMap { index, result -> T? in switch result { case let .success(value): return value // 3. Добавим errorInfo и его передачу через callback case let .failure(error): var errorInfo = [String: Any]() errorInfo["error"] = error errorInfo["index"] = index onItemError?(errorInfo) return nil } } if filtredArray.isEmpty { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Empty array of elements is not allowed") } return filtredArray }
}

Теперь у нас есть возможность использовать вывод описания ошибок декодинга в нашем propertyWrapper.

Финальный шаг. Реализуем propertyWrapper:

@propertyWrapper
public struct SafeArray<T: Decodable>: Decodable { public let wrappedValue: [T] public init(wrappedValue: [T]) { self.wrappedValue = wrappedValue } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = try container.safelyDecodeArray() }
}

Это всё, что нужно сделать, чтобы использовать обёртку для безопасного декодинга массивов. Теперь можно помечать массивы как SafeArray — и всё заработает автоматически.

Код из статьи целиком вы найдёте в последнем разделе.

Дополнительные propertyWrappers

В примере мы парсили массив в константу. Если нам нужно менять массив после парсинга, достаточно заменить в обёртке let на var, потому что обёрнутое свойство должно быть таким же, как wrappedValue:

@propertyWrapper
public struct SafeMutableArray<T: Decodable>: Decodable { public var wrappedValue: [T] ...
}

Тогда свойство тоже можно будет сделать переменной:

struct Example: Decodable { @SafeMutableArray var articlesArray: [Article]
}

Если нам нужно получить опциональный массив, необходимо добавить опциональность и для wrappedValue:

@propertyWrapper
public struct SafeOptionalArray<T: Decodable>: Decodable { public let wrappedValue: [T]? public init(wrappedValue: [T]?) { self.wrappedValue = wrappedValue } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = try? container.safelyDecodeArray() }
}

Чтобы после декодинга опциональный массив можно было изменить, достаточно снова заменить let wrappedValue на var wrappedValue.

Вместо вывода

Это был, на мой взгляд, не самый очевидный способ декодирования данных в Swift, однако это ещё не все возможности property Wrapper.

Так как обёртки используются в структурах и классах, то их можно попробовать использовать в любом месте приложения, добавляя любую нужную логику, которую можно уместить.

Но всегда помните о том, что большая сила влечёт за собой и большую ответственность: если оставить внутри обёртки сложную логику, то она может аукнуться неочевидным поведением обёрнутого свойства. Применяйте инструмент там, где это действительно необходимо и к месту.

Код из статьи
@propertyWrapper
public struct SafeArray<T: Decodable>: Decodable { public let wrappedValue: [T] public init(wrappedValue: [T]) { self.wrappedValue = wrappedValue } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() wrappedValue = try container.safelyDecodeArray(onItemError: nil) }
} extension SingleValueDecodingContainer { func safelyDecodeArray<T>(onItemError: (([String: Any]) -> Void)?) throws -> [T] where T: Decodable { let decodedArray = (try? decode([Throwable<T>].self)) ?? [] let filtredArray = decodedArray.enumerated().compactMap { index, result -> T? in switch result { case let .success(value): return value case let .failure(error): var errorInfo = [String: Any]() errorInfo["error"] = error errorInfo["index"] = index onItemError?(errorInfo) return nil } } if filtredArray.isEmpty { throw DecodingError.dataCorruptedError(in: self, debugDescription: "Empty array of elements is not allowed") } return filtredArray }
} enum Throwable<T: Decodable>: Decodable { case success(T) case failure(Error) init(from decoder: Decoder) throws { do { let decoded = try T(from: decoder) self = .success(decoded) } catch let error { self = .failure(error) } }
}

Читайте так же:

  • Как заработать на партнерке: обзор ниши essay на рынке СНГ от Vsesdal.com Партнерский материал Конечно, чтобы заработать на рефератах, необязательно их писать самому. В статье речь пойдет о заработке на партнерских программах.  Рынок онлайн-образования резко изменился в условиях пандемии, и, похоже, что образовательные учреждения не спешат […]
  • Стали известны характеристики камер флагманского смартфона Samsung Galaxy Z Fold 3 с гибким экраномСтали известны характеристики камер флагманского смартфона Samsung Galaxy Z Fold 3 с гибким экраном В Сети появились подробности о камерах складного смартфона Samsung Galaxy Z Fold 3. Из новой утечки следует. Что компания не собирается отказываться от 12-мегапиксельных датчиков. Которые уже давно стали символом флагманов Samsung. Всего у Galaxy Z Fold 3 три камеры. Самая […]
  • РСЯ от А до Я: как сделать удобную структуру рекламного аккаунта в Директе Выпускающий редактор SEOnews На прошлой неделе интернет-издание SEOnews и сервис по работе с интернет-рекламой eLama запустили видеокурс «РСЯ от А до Я». За 9 видеоуроков мы научим вас настраивать эффективные кампании в Рекламной сети Яндекса. В первом уроке мы […]
  • Управление контентом на сайтеУправление контентом на сайте Система управления веб-контентом (WCM или WCMS)[1] - это программная система управления контентом (CMS). Предназначенная специально для веб-контента. Он предоставляет инструменты для создания веб-сайтов. Совместной работы и администрирования. Которые помогают пользователям с […]