Как дождаться загрузки данных с Firestore, а потом продолжить работу


#1

Доброго времени суток.
Делаю первое приложение, из БД выбрал Firestore. Загрузил данные в базу. Возникла проблема с загрузкой данных из базы.
База выглядет так:

В коллекции belts у меня есть объекты(Документы) и в них есть поля (массивы), которые содержат ссылки на другие объекты из БД.

Когда создаю модель Belt в программе, для заполнения его полей, беру объекты из массива c помощью функции:

В частности функция getBaseTechnique

Она возвращает пустой массив. Но спустя какое-то время выполняется print, который находится в цикле.
Я понимаю, что это происходит потому, что запросы делаются асинхронно.

Пытался разобраться с потоками, но не получилось.

Подскажите способ приостановить работу программы до загрузки данных.


#2

@escpaping ))) Тут точно уже были такие вопросы, имею ввиду на форуме)

Вам необходимо сделать всю работу внутри блока, либо можете создать переменную, с типом который ожидаете от firestore и повесить на него наблюдателя)) как удобней))


#3
protocol SomeProtocol {
   var dictionary: [String: Any ] { get }
   init?(dictionary: [String: Any])
}    

func getOneDocument<T: SomeProtocol>(collection: CollectionReference, document: String, completion: @escaping(T?, Error?)->Void) {
        var snapshotData: T?
        collection.document(document).getDocument { (snapshot, error) in
            if let snapshot = snapshot {
                snapshotData = T(dictionary: snapshot.data() ?? [:])
            }
            completion(snapshotData, error)
        }
    }

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


#4

Спасибо за ответ.
У меня возникает проблема именно с обработкой данных. Они приходят с запозданием.

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

Я переписал функцию вот так:

и вызываю ее таким образом:

В принципе все работает как и до этого. В первом скрине выводы принтов в консоли.

Как мне сделать так, чтобы в массив добавлялись уже загруженные модели?


#5

Переписал еще раз функцию, как в вашем примере. Теперь замыкание и функция ничего не возвращают. Но работает так же.

Вызов функции:

Сама функция с консолью:


#6

Так, стоп, я обчитался в вашем коде, а может и нет, что такое baseTex…Arr? Это ваш массив моделек, который внутри класса? или внутри функции?

Что должно измениться?


#7

У меня есть модель Belt, в которой есть поле(свойство) baseTecnique. (Первый скриншот поможет разобраться).
Это свойство - массив ссылок на другие объекты в БД, а в приложении мне нужны сами объекты.

Я хочу взять данные из БД и создать модель Belt, одно из свойств которое baseTecnique, Которое содержит массив [BaseTecnique].

Так вот. Когда я беру данные из базы из модели Belt в поле baseTecnique, я получаю ссылки на объекты.
В функции getBaseTechnique получаю эти объекты из БД и в замыкании создаю сами объекты и добавляю их в baseTecniqueArr, но они не добавляются.
Далее при создании Belt присвоить полю baseTecnique значение массива baseTecniqueArr.


#8

А можете плиз прям всю страничку скинуть, не скрином


#10
import UIKit

import Firebase

class BeltViewController: UIViewController {

var completedRequests = false
var belts = [String]()
let db = Firestore.firestore()
@IBOutlet weak var segmentedControl: UISegmentedControl!

override func viewDidLoad() {
    super.viewDidLoad()
    
    segmentedControl.replaceSegments(segments: belts)
    segmentedControl.selectedSegmentIndex = 0
    title = segmentedControl.titleForSegment(at: segmentedControl.selectedSegmentIndex)
    
    getBelts(belts)
    
}

@IBAction func backTapped(_ sender: UIBarButtonItem) {
    dismiss(animated: true, completion: nil)
}


func getBelts(_ beltsArray: [String]) -> [Belt] {
    var resultArray = [Belt]()
    
    for beltName in beltsArray {
        db.collection("belts").document(beltName).getDocument { [weak self] (belt, error) in
            
            var baseTechniqueArr = [BaseTechnique]()
            let combination: [Combination]?
            let description: String?
            let form4sides: [Form4sides]?
            let gupDan: String
            let hosinsul: [Hosinsul]?
            let image: UIImage
            let ofp: [Ofp]?
            let sogi: [Sogi]?
            let sparring: [Sparring]?
            let specialTechnic: [SpecialTechnic]?
            let theory: [Theory]
            let title: String
            let tuls: [Tul]?
            
           
            if error == nil {
                    if belt != nil && belt!.exists {
                        
                        let beltData = belt!.data()
                        
                        self!.getBaseTechnique(baseTechniqueArray: (beltData!["baseTechnique"] as! Array<DocumentReference>), completion: { (snapshot, error) in
                            
                            let baseTechniqueObj = BaseTechnique(title: (snapshot?["title"]! as! String),
                                                                 description: (snapshot?["description"]! as! String),
                                                                 image: UIImage(named: snapshot?["image"]! as! String)!)
                            
                            print("1) Модель в замыкании", baseTechniqueObj)
                            baseTechniqueArr.append(baseTechniqueObj)
                            
                        })
                        
                        print("3) Массив моделей, которые должен добавить в Belt", baseTechniqueArr)
                        
                        
                        gupDan = beltData!["gupDan"] as! String
                        image = UIImage(named: beltData!["image"] as! String)!
                        theory = self!.getTheory(theoryArray: beltData!["theory"]! as! Array<DocumentReference>)
                        title = beltData!["title"] as! String
                        
                        
                        tuls = nil
                        specialTechnic = nil
                        sogi = nil
                        ofp = nil
                        hosinsul = nil
                        description = nil
                        form4sides = nil
                        sparring = nil
                        combination = nil
                        
                        
                        let result = Belt(title: title,
                                          description: description,
                                          image: image,
                                          baseTechnique: baseTechniqueArr,
                                          form4sides: form4sides, gupDan: gupDan,
                                          hosinsul: hosinsul,
                                          ofp: ofp,
                                          sogi: sogi,
                                          sparring: sparring,
                                          comination: combination,
                                          specialTechnics: specialTechnic,
                                          theory: theory,
                                          tuls: tuls)
                        

                        


                }
            }
        }
        break
    }
    
    return resultArray
    
}

func getBaseTechnique(baseTechniqueArray: Array<DocumentReference>?, completion: @escaping([String: Any]?, Error?)->Void) {
    
    guard let baseTechniqueArr = baseTechniqueArray else { return }
   
    baseTechniqueArr.forEach { (element) in
    var snapshotData = [String: Any]()
        
    element.getDocument { (baseTechniqueSnapshot, error) in
        if let baseTechniqueDict = baseTechniqueSnapshot?.data() {
            snapshotData = baseTechniqueDict
            
            completion(snapshotData, error)
            }
        }
    }
}

func getTheory(theoryArray: Array<DocumentReference>) -> [Theory] {
    
    var result = [Theory]()
    
    for element in theoryArray {
        
        element.addSnapshotListener { (theorySnapshot, error) in

            let theoryDict = theorySnapshot?.data() as! [String: String]
            let theory = Theory(questions: theoryDict)
            
            result.append(theory)
        }
    }
    return result
}

}

extension UISegmentedControl {
func replaceSegments(segments: Array) {
self.removeAllSegments()
for segment in segments {
self.insertSegment(withTitle: segment, at: self.numberOfSegments, animated: false)
}
}
}

Как-то разорвался код. Я не пойму почему, если честно.
За оформление сильно не ругайте.
Это мое первое приложение.


#11

ну у вас тут задача усложняется тем что вы проходите по массиву и на каждый элемент делаете запрос

Можете сделать так, создать пустой массив типа loadBelts: [Belt] = [] внутри класса, и при получение ответа по каждому ответу из beltsArray, добавляйте полученный ответ в self?.loadBelts, все это внутри блока, либо можете подумать и сделать подсчёт все ли объекты загрузились и через комплетишен выкинуть сразу весь собранный массив, но тут еще нужна обработка ошибок, а то можно вечно ожидать его)) Самый простой вариант это первый))

Но попробуйте по возможности избавиться от массивы запросов


#12

Я вам поэтому добавил в функцию “сбегающий” клоужер (@escaping) - он будет выполняться после загрузки данных из сети, и в клоужере уже обработаете готовые результаты.


#13

Попробовал, результат тоже. Думаю.


#14

А я обрабатываю в сбегающем клоужере. Но результат тот же. Или я что-то неправильно делаю?

Вот в этом ответе обработка.


#15

Мне в вашем примере не очень нравится то,что из функции с одним запросом вы сделали параллельный множественный запрос (в функцию getBaseTechnique передаёте массив ссылок на объекты бд и потом в одном клоужере их обрабатываете) - так не получится скорее всего. Лучше сделать как я вам предлагал: функция из моего примера получает один документ и его потом обрабатываете. Соответственно, если вам надо обработать массив, то вы в цикле перебираете массив и для каждого объекта вызываете функцию получения документа из БД
с @escaping, чтобы у вас получились отдельные запросы, каждый из которых будет обработан в своём клоужере в внутри одного шага цикла (надеюсь понятно описал :slight_smile: ) - тогда запросы получатся поочередно и каждый последующий будет ждать выполнения предыдущего.


#16

Я немного другое решение нашел. Но данные нужно подгружать с другого контроллера и отправлять в контроллер, в котором сейчас идет обработка. Но мне не очень нравиться.

Попробую еще как вы описали.

Чуть позже напишу что получилось.


#17

Получилось!!! Использовал, мне кажется, все что вы советовали, плюс немного в сети накопал.

@voragomod, @ODiN спасибо за поддержку. :handshake: Я уже, грешным делом, задумывался к БД менять.

Модель Belt выглядит вот так:

struct Belt {
    
    var title: String
    var baseTechnique: [BaseTechnique]?
    var tuls: [Tul]?

}

Все разворачивается в одном классе. Потом, наверное, нужно будет отдельный файл создавать для загрузки

import UIKit
import Firebase

class BeltViewController: UIViewController {
    
    // Получаю данные для этого массива из другого контроллера
    var beltsArrString = [String]()
    // В этом массив соберутся нужные мне модели, которые я буду отображать на экране.
    var beltsArrObj = [Belt]()
    // массив с флагами. Нужен для того, чтобы добавить модель в массив beltsArrObj, когда все данные загружены
    var beltDownloadComplete = [false, false]
    //Создаю модель  Belt в которой постепенно сохраняю данные, когда они подгружаются, она на самом деле больше, урезал для примера
    var belt = Belt(title: "Title", baseTechnique: nil, tuls: nil)
    let db = Firestore.firestore()
    
    
    @IBOutlet weak var beltDescription: UILabel!
    @IBOutlet weak var imageOutlet: UIImageView!
    @IBOutlet weak var segmentedControl: UISegmentedControl!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        segmentedControl.replaceSegments(segments: beltsArrString)
        segmentedControl.isEnabled = false
        self.title = "adfvadfvavd"
        activityIndicator.startAnimating()
//    Беру данные для первого пояса. Раньше это делал в цикле, но с ним много сложностей. Поэтому решил брать их по одному
        getBelt(beltsArrString.removeFirst())
    }
    
//    в этой фунции делаю запросы к базе по каждому свойству модели Belt
    func getBelt(_ beltString: String) {
        
        db.collection("belts").document(beltString).getDocument { [weak self] (belt, error) in
            guard error == nil else {
                print(error!.localizedDescription)
                return
            }
            
            if belt != nil && belt!.exists {
                let beltData = belt!.data()
                self!.getBaseTechnique(baseTechniqueArray: (beltData!["baseTechnique"] as! Array<DocumentReference>))
                self!.getTul(tulArray: (beltData!["tuls"] as! Array<DocumentReference>))
            }
        }
    }
    
// Беру данные для свойства baseTechnique
    func getBaseTechnique(baseTechniqueArray: Array<DocumentReference>?) {
       
        guard let baseTechniqueArr = baseTechniqueArray else { return }
        
        var result = [BaseTechnique]()
        let count = baseTechniqueArr.count
        
        baseTechniqueArr.forEach { (element) in
//            вызываю функцию createObject, которая обращается к базе и в замыкании создаю объект
            createObject(element: element) { (dict, error) in
                
                let baseTechniqueObj = BaseTechnique(title: (dict!["title"]! as! String),
                                                     description: (dict!["description"]! as! String),
                                                     image: UIImage(named: dict!["image"]! as! String)!)
                result.append(baseTechniqueObj)
//               Если количество созданныъ элементов равно исходному
                if count == result.count {
//                присваиваю массив свойству baseTechnique
                    self.belt.baseTechnique = result
//                  Устанавливаю для одного из флагов значение true. C этим массивом поработаем в loadBeltToArr
                    self.beltDownloadComplete[0] = true
                    self.loadBeltToArr()
                }
            }
        }
    }
    
//    Для взятия getTul принцип работы тот же что и для getBaseTechnique, расписывать не буду
    func getTul(tulArray: Array<DocumentReference>?) {
        
        guard let tulArr = tulArray else { return }
        
        var result = [Tul]()
        let count = tulArr.count
        
        tulArr.forEach { (element) in
                createObject(element: element) { (dict, error) in
                    
                    let tulObj = Tul(title: (dict!["title"]! as! String),
                                     description: (dict!["description"]! as! String),
                                     image: UIImage(named: dict!["image"]! as! String)!)
                    result.append(tulObj)
                    if count == result.count {
                        self.belt.tuls = result
                        self.beltDownloadComplete[1] = true
                        self.loadBeltToArr()
                }
            }
        }
    }
    
//    Создаю объект передаю сюда только ожин элемент с ним и работаю. Все запросы обрабатываюся последовательно благодаря completion: @escaping([String: Any]?, Error?)->Void)
//    Важно. Так как я точно знаю на тестовых данных что мне придет, я не делаю проверки на ошибки. Но она тут обязательно нужно и в getTul, getBaseTechnique тоже.
    func createObject(element: DocumentReference, completion: @escaping([String: Any]?, Error?)->Void) {
        element.getDocument { (snapshot, error) in
            if let dict = snapshot?.data() {
                completion(dict, error)
            }
        }
    }
    
// И в getTul, getBaseTechnique, я вызываю loadBeltToArr, она срабатывает, только если две функции отработали, благодаря проверке !beltDownloadComplete.contains(false)
    
    func loadBeltToArr() {
        
        if !beltDownloadComplete.contains(false) {
            beltsArrObj.append(belt)
            
            if !beltsArrString.isEmpty {
                beltDownloadComplete = [false, false]
//                если переданный в контроллер массив не пустой, беру следующий пояс.
                getBelt(beltsArrString.removeFirst())
            } else {
//                Когда все закончено отображаю данные.
                activityIndicator.stopAnimating()
                activityIndicator.isHidden = true
                self.segmentedControl.isEnabled = true
                self.segmentedControl.selectedSegmentIndex = 0
                self.choiceSegment(segmentedControl)
            }
        }
}    ... продолжение класса, к теме не относится.

#18

Ну вот :slight_smile::+1: