Предисловие

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


  1. house2008
    02.12.2021 19:12

    Возьмите Rx/Combine/await+async, чем городить своё. Не вижу довольно обычного кейса, когда один запрос отвалился и нужно остальное отменить. count и dictonary обновляются в разных потоках, предполагаю, что может быть race conditon и done никогда не вызовется. Пока больше похоже на вредные советы.


    1. cbepxbeo Автор
      02.12.2021 19:26

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


      1. house2008
        02.12.2021 19:39

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


        1. cbepxbeo Автор
          02.12.2021 19:48

          Если Вам подобное интересно, то можно и заморочиться.) Но конкретно в этой публикации мои мотивы несколько в другом, я очень радею за популизацию языка в России и Вы же понимаете, что доступной информации очень мало. А для новоиспеченных разработчиков, так и вовсе не найти. Все ограничивается туториалами про различие переменных и констант. В примере я хотел обозначить несколько моментов. Практический пример передачи замыканий, использование didSet, обращение к экземпляру по цепочке, примитивы работы с сетью. Мне кажется получилось не плохо. А вот с предупреждениями конечно прогадал. Когда я готовил материал, мне казалось очевидным, что это далеко не рабочее решение.

          Дженерики я не очень жалую если честно, я законченный адепт kiss. Если и реализовываю что-либо, то стараюсь делать это изолировано и под определенные задачи со строго определенным типом. Понятно, что без них не обойтись, но всегда стараюсь свести их количество к минимуму, но это уже мои сугубо личные предпочтения.


  1. FreeNickname
    02.12.2021 19:23

    Обратите внимание, что Swift-овый нативный async/await, который сейчас доступен в iOS 15+, будет доступен в iOS 13+ в следующем релизе Xcode. И там всё это делается гораздо-гораздо проще.


  1. flyer2001
    03.12.2021 09:23

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


    1. cbepxbeo Автор
      03.12.2021 13:51
      +1

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


      1. Gargo
        04.12.2021 20:59
        -1

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

        Другое существенное отличие от готовых решений - "dictionary" там бы передавался параметром в completion, а у вас нарушается принцип single responsibiliy - один класс и тот же класс используется и для конструирования последовательности запросов, и для хранения промежуточных данных.


        1. cbepxbeo Автор
          05.12.2021 00:48
          +1

          А Вы предисловие, читали перед подобным комментарием? Это код написан за 10 минут в информационных целях как наглядная демонстрация возможностей языка. Для новичков в разработке. Не более. Наглядно, быстро, по факту. Плюс я сам писал, что это костыли. Насколько было можно склеить воедино различные плюшки Swift, настолько я и склеил. Или вы предлагаете обернуть все в протоколы с расширениями, на дженериках и т.д. но тогда это уже будет не учебный материал. Перечитайте, будьте любезны.