Предисловие
Материал является примером, который демонстрирует возможности языка и позволяет глубже понять работу различных фреймворков (например 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...