Предисловие
Материал является примером, который демонстрирует возможности языка и позволяет глубже понять работу различных фреймворков (например 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...
Комментарии (9)
FreeNickname
02.12.2021 19:23Обратите внимание, что Swift-овый нативный async/await, который сейчас доступен в iOS 15+, будет доступен в iOS 13+ в следующем релизе Xcode. И там всё это делается гораздо-гораздо проще.
flyer2001
03.12.2021 09:23лучше с бэком договориться))) все равно подобные решения костылеподобны, как красиво не оборачивай. Порождает кучу краевых и чем больше последовательных запросов - краевые растут в геометрической прогрессии.
cbepxbeo Автор
03.12.2021 13:51+1Согласен, на беке реализовать будет проще. Но Вы посмотрите на код, это не последовательные запросы, выполняются они асинхронно. Конечно в примере реально костыльный код, он больше для ознакомления, но общую его концепцию можно развить при желании. Можно отлавливать провалившиеся запросы и ставить их в очередь повторной загрузки по определенным критериям, с защитой от бесконечной рекурсии и т.д. Combine работает тоже весьма неплохо. Да и в целом, если поднажать, можно вполне себе годный сетевой слой реализовать, с кэшированием и т.д. вопрос мотивации и необходимости.
Gargo
04.12.2021 20:59-1код очень костыльный - вы описываете "completion", который должен как бы поставить точку в обработке запросов, но потом пытаетесь прикрепить еще какую-то обработку - хотя бы назовите этот блок как-нибудь по-другому. Если же использовать completion блок по назначению, то тогда нет смысла хранить массив блоков.
Другое существенное отличие от готовых решений - "dictionary" там бы передавался параметром в completion, а у вас нарушается принцип single responsibiliy - один класс и тот же класс используется и для конструирования последовательности запросов, и для хранения промежуточных данных.
cbepxbeo Автор
05.12.2021 00:48+1А Вы предисловие, читали перед подобным комментарием? Это код написан за 10 минут в информационных целях как наглядная демонстрация возможностей языка. Для новичков в разработке. Не более. Наглядно, быстро, по факту. Плюс я сам писал, что это костыли. Насколько было можно склеить воедино различные плюшки Swift, настолько я и склеил. Или вы предлагаете обернуть все в протоколы с расширениями, на дженериках и т.д. но тогда это уже будет не учебный материал. Перечитайте, будьте любезны.
house2008
Возьмите Rx/Combine/await+async, чем городить своё. Не вижу довольно обычного кейса, когда один запрос отвалился и нужно остальное отменить. count и dictonary обновляются в разных потоках, предполагаю, что может быть race conditon и done никогда не вызовется. Пока больше похоже на вредные советы.
cbepxbeo Автор
Да кто же спорит). Я же специально написал, что это пример реализации для того, чтобы было понятно как и что работает. Вы вероятно не представляете, сколько людей в лоб пользуются решениями/библиотеками и т.д. абсолютно не вникая в происходящее. Для этого и я набросал этот фейкокод. Возможно мало предупреждений добавил, поправлю.
house2008
Просто вы сделали самую легкую часть, это не интересно) Намного сложнее и важнее как раз детали, ради чего я и зашел прочитать статью. Думал тут увижу супер умный сервис, который по определенный правилам собирается параллельно данные с разных web сервисов и мержит результат и всё на протоколах и дженериках, а еще лучше await/async.
cbepxbeo Автор
Если Вам подобное интересно, то можно и заморочиться.) Но конкретно в этой публикации мои мотивы несколько в другом, я очень радею за популизацию языка в России и Вы же понимаете, что доступной информации очень мало. А для новоиспеченных разработчиков, так и вовсе не найти. Все ограничивается туториалами про различие переменных и констант. В примере я хотел обозначить несколько моментов. Практический пример передачи замыканий, использование didSet, обращение к экземпляру по цепочке, примитивы работы с сетью. Мне кажется получилось не плохо. А вот с предупреждениями конечно прогадал. Когда я готовил материал, мне казалось очевидным, что это далеко не рабочее решение.
Дженерики я не очень жалую если честно, я законченный адепт kiss. Если и реализовываю что-либо, то стараюсь делать это изолировано и под определенные задачи со строго определенным типом. Понятно, что без них не обойтись, но всегда стараюсь свести их количество к минимуму, но это уже мои сугубо личные предпочтения.