Как стать автором
Обновить

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

Время на прочтение4 мин
Количество просмотров3.8K
Предисловие

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

Теги:
Хабы:
+1
Комментарии11

Публикации

Изменить настройки темы

Истории

Работа

iOS разработчик
23 вакансии
Swift разработчик
32 вакансии

Ближайшие события