Swift. Сборка данных из разных запросов

от автора

Предисловие

Материал является примером, который демонстрирует возможности языка и позволяет глубже понять работу различных фреймворков (например Combine), делающих работу с сетью более простой.

Материал предназначен начинающим разработчикам для общего ознакомления.

Все примеры в публикации являются крайне упрощенной версией кода и их не стоит использовать при разработке приложений в том виде, в котором они есть. Даже в доработанном виде, код не составляет конкуренции существующим решениям и создан исключительно в ознакомительных целях. А так же не забывайте, что все операции производимые с языком Swift должны быть безопасными, в примере о ней не может идти и речи.

Весь свой информационный мусор, я коллекционирую на своей стене в ВК, так что добро пожаловать.

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

Эти данные мы не можем собрать по ряду определенных причин в рамках одного запроса, поэтому нашей задачей является выполнить несколько запросов и после того, как все они выполнятся (мы получим данные), произведем какие-либо действия. Выполнять действия до получения всех данных нельзя, порядок получения данных нам неизвестен т.к. это асинхронные операции и объем данных так же неизвестен, как и скорость сети и т.д. думаю, в пояснении не нуждается.

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

Что же, начнем. Создадим класс MyHttpExample

class MyHttpExample {      }

И добавим ему метод request

func request(){}

метод будет открытым

public func request(){}

и будет принимать в себя два аргумента

public func request(url urlString: String, key: String){}

ссылку типа String и ключ, назначение которого вы поймете чуть позже.

В методе мы будем использовать URLSession для отправки запросов, но предварительно нам нужен удобный тип данных, который удовлетворяет нашим запросам, поэтому создадим в классе MyHttpExample обертку в виде типа данных Wrapper

struct Wrapper { let response: URLResponse let data: String }

так как это пример, мы не будем получать настоящую Data, а обойдемся ее имитацией, URLResponse же возьмем, дабы убедится в исполнении кодом своих задач. Теперь вернемся к методу request

public func request(url urlString: String, key: String) -> MyHttpExample {     guard let url = URL.init(string: urlString) else { return self }     let session = URLSession.shared     let task = session.dataTask(with: url){ (_, response, _) in         //различные проверки     }     task.resume()     return self }

полагаю пояснять особо нечего, формируем задачу и обрабатываем завершение. Зачем метод возвращает текущий экземпляр (мы будем использовать экземпляры) поймете чуть позже. Теперь давайте добавим словарь, который будет хранить наши данные и счетчик запросов

private var count = 0 public var dictonary: [String: Wrapper] = [:]

в качестве наблюдателя будем использовать свежесозданный словарь.

public var dictonary: [String: Wrapper] = [:] { didSet {} }

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

private var completions: [() -> Void]? = []      private func done(){}

свойство делаем опциональным т.к. для извлечения данных из словаря мы будем использовать передаваемое замыкание, в которое будем передавать сам экземпляр класса куда ранее передали замыкание, а т.к. классы и замыкания представляют собой ссылочный тип данных, подобное приведет к утечке памяти (замкнутый цикл ссылок), но мы самостоятельно передадим ему nil по завершению, что позволит сборщику мусора избавится от ненужного экземпляра.

далее создадим метод добавления замыканий в массив.

public func addClosure(_ closure: @escaping () -> Void) -> MyHttpExample{     self.completions?.append {         closure()     }     return self }

Теперь можем задать поведение наблюдателю сверяя количество данных в словаре со счетчиком запросов, который мы будем повышать при каждом вызове метода request

public var dictonary: [String: Wrapper] = [:] {     didSet {         if dictonary.count == self.count {             self.done()         }     } }

опишем метод done

private func done(){     for completion in completions ?? [] {         completion()     }     completions = nil }

и доработаем метод request

public func request(url urlString: String, key: String) -> MyHttpExample {     self.count += 1     guard let url = URL.init(string: urlString) else { return self }     let session = URLSession.shared     let task = session.dataTask(with: url){ (_, response, _) in         //различные проверки         self.dictonary[key] = Wrapper(response: response!, data: key)     }     task.resume()     return self }

в качестве даты мы передаем ключ, для примера этого будет достаточно.

обратите внимание

мы не проверяем response и принудительно извлекаем, не обрабатываем ошибки и т.д. т.к. это пример, соответственно не следует использовать этот код без доработок

По итогу мы получим вот такой код

class MyHttpExample {          private var count = 0     public var dictonary: [String: Wrapper] = [:] {         didSet {             if dictonary.count == self.count {                 self.done()             }         }     }          private var completions: [() -> Void]? = []          private func done(){         for completion in completions ?? [] {             completion()         }         completions = nil     }          public func addClosure(_ closure: @escaping () -> Void) -> MyHttpExample{         self.completions?.append {             closure()         }         return self     }          public func request(url urlString: String, key: String) -> MyHttpExample {         self.count += 1         guard let url = URL.init(string: urlString) else { return self }         let session = URLSession.shared         let task = session.dataTask(with: url){ (_, response, _) in             //различные проверки             self.dictonary[key] = Wrapper(response: response!, data: key)         }         task.resume()         return self     }          struct Wrapper {         let response: URLResponse         let data: String     } }

Теперь давайте его проверим. Сперва на получение response

добавим деинициализатор, чтобы отслеживать состояние экземпляра в будущем, а сейчас побалуемся

код я немного сжал(игнорирование отступов), чтобы поместилось на скрин

Поведение соответствует ожиданию.

Теперь добавим элемент задержки, чтобы убедится в уничтожении экземпляра, а так же посмотреть работает ли замыкание после деинициализации, собственно для этого мы запустим код в составе приложения на устройстве.

Xcode ругается т.к. не может рассчитать результат кода.

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

See you later…


ссылка на оригинал статьи https://habr.com/ru/articles/593053/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *