Swift имеет много интересных особенностей, одна из которых — FlatMap. Эта интересная особенность способна превратить не самый красивый код в великолепный.
В примере, на котором мы рассмотрим FlatMap мы будем получать данные в формате JSON и преобразуем их в массив. JSON-файл, с которым мы будем работать, лежит в папке Resources в Playground, который вы можете скачать в конце статьи.
Функция, которую мы напишем, будет асинхронна, поэтому в ней будет замыкание, возврщающее результат или ошибку.
FlatMap: Приступим
Для начала напишем небольшую структуру, описывающую данные, которые мы собираемся получать.
1 2 3 4 5 | struct SomeData { var identifier: String var title: String var description: String } |
После этого давайте напишем расширение для структуры, в котором создадим инициализатор, принимающий словарь для заполнения полей структуры.
1 2 3 4 5 6 7 8 9 10 | extension SomeData { init?(with dictionary: [String: Any]) { guard let identifier = dictionary["id"] as? String, let title = dictionary["title"] as? String, let description = dictionary["description"] as? String else { return nil } self.identifier = identifier self.title = title self.description = description } } |
FlatMap: Получение данных с сервера
Первым делом, давайте создадим список возможных ошибок, с которыми программа может столкнуться при обращении к серверу:
1 2 3 4 5 | enum ServiceError: Error { case invalidData case noData case unknown } |
И сейчас мы объявим замыкание, которое по выполнению будет возвращать нам либо ошибку, либо массив данных, описанных в структуре выше:
1 | typealias FeatchProgramCompletionHandler = ([SomeData]?, Error?) -> Void |
Напишем типичный метод для загрузки данных с сервера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | func featchSomeData(from url: URL, completionHandler: @escaping FeatchSomeDataCompletionHandler) { let urlSession = URLSession.shared let dataTask = urlSession.dataTask(with: url) { data, _, error in guard let data = data else { completionHandler(nil, error) return } guard let jsonArray = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [[String: Any]] else { completionHandler(nil, ServiceError.invalidData) return } var results = [SomeData]() for dictionary in jsonArray { guard let result = SomeData(with: dictionary) else { continue } results.append(result) } completionHandler(results, nil) } dataTask.resume() } |
Наш код в порядке, он работает, как и ожидалось. Но он не выглядит хорошо. Мы можем немного улучшить его.
FlatMap: Swift-способ
1 2 3 4 5 6 7 8 9 10 11 12 | func featchSomeData(from url: URL, completionHandler: @escaping FeatchSomeDataCompletionHandler) { let urlSession = URLSession.shared let dataTask = urlSession.dataTask(with: url) { data, _, error in let jsonArray = data.flatMap { try! JSONSerialization.jsonObject(with: $0, options: .allowFragments) as? [[String: Any]]} let results = jsonArray?.flatMap(SomeData.init(with:)) completionHandler(results, error) } dataTask.resume() } |
Что же происходит в коде?
1 | let jsonArray = data.flatMap { try! JSONSerialization.jsonObject(with: $0, options: .allowFragments) as? [[String: Any]]} |
Этот код применяет flatMap к данным, пытаясь преобразовать данные в json-массив словарей. Этот код возвратит либо nil, либо заполненный json-массив словарей.
1 | let results = jsonArray?.flatMap(SomeData.init(with:)) |
Здесь на массив json мы применили flatmap и передали функцию, которая будет выполняться для каждого элемента. Каждый элемент — это [String: Any], так что SomeData.init(with:) — идеальный путь для приведения к flatMap. Мы возвратили массив [SomeData]?, и у нас не может возникнуть ошибок в процессе, так как faltMap отсеет все nil, так что любая неудача при загрузке на конечно результате не отразится.
Посмотрите на код, который мы написали выше (типичный метод для загрузки данных с сервера), и теперь посмотрите на новый flatmap-код. Мы обошлись без if, guard. И нам не нужно ожидать, пока метод получит и вернет весь результат, в надежде на то, что все данные будут получены успешно.
Итак, в итоге у нас есть опциональный массив SomeData и дополнительный объект-ошибка, которые мы можем передать в замыкание.