Cloud Firestore


#1

Создам тему, тк ковыряю Firestore и вопросов много, но начну по чуть-чуть.

Требуется помощь гуру.

Использую структуру для работы с данными и кастомный инициализатор, сделанный через экстеншен:

struct MainUser {
    let uid: String
    var email: String
    var firstName: String
    var secondName: String
    var friends: [String]
    var role: String
    
    var dictionary: [String: Any] {
        return ["uid": uid, "email": email, "firstName": firstName, "secondName": secondName, "friends": [friends], "role": role]
    }
}


  extension MainUser {
    init?(dictionary: [String: Any]) {
        guard let uid = dictionary["uid"] as? String,
            let email = dictionary["email"] as? String,
            let firstName = dictionary["firstName"] as? String,
            let secondName = dictionary["secondName"] as? String,
            let friends = dictionary["friends"] as? [String],
            let role = dictionary["role"] as? String else {return nil}

        self.init(uid: uid, email: email, firstName: firstName, secondName: secondName, friends: friends, role: role)
    }
}

Главная проблема:
Firestore позволяет создавать значения в документах в массивах
43
Обратите внимание на проперти friends в структуре MainUser. Обычный массив. При инициализации используется вычислямое свойство dictionary.
Так вот, при создании документа let newUser = MainUser(uid: "", email: email, firstName: firstName, secondName: secondName, friends: [], role: "") даже с пустым массивом, Firestore ругается следющим сообщением: “Nested arrays are not supported” - “Вложенные массивы не поддерживаются”. Но вроде же создаётся обычный массив, а не вложенный… По ощущениям ошибка в инициализаторе, но не пойму где. Нужна помощь :slight_smile:

Вопрос 2 (неважный):
при данной структуре и экстеншене почему-то не появляется кастомный инициализатор при создании объекта структуры
37

прописываю вручную - всё работает. Хз где ошибка…

З.Ы при закомменчивании данного проперти (friends), всё работает нормально и создаётся запись в Firestore.

let newUser = MainUser(uid: "", email: email, firstName: firstName, secondName: secondName, /*friends: [],*/ role: "")
            
            let ref = db.collection("users").addDocument(data: newUser.dictionary) {

Ессно закоментил везде, где надо


Обработка ошибок, Firestore
Вопрос по Уведомлениям (Notifications)
#2

Так иногда бывает: написал вопрос и сам под влиянием этого вопроса разобрался где ошибка:
вдруг кому пригодится:
Тк в структруре MainUser проперти friends и так массив, то в dictionary надо вычилсять "friends": friends, а не "friends": [friends]

З.Ы. Второй вопрос, соотвественно, так же решился: при исправлении данной ошибки появился нужный инициализатор

Ждите ещё вопросов самому себе :smile:


#3

Второй вопрос по Firestore.
Позволено хранить тип данных Date, но: Data type: Date and time; Sort order: Chronological; Notes: When stored in Cloud Firestore, precise only to microseconds; any additional precision is rounded down.

Соотвественно в базу заметательно записывается тип Date, но при извлечении эти данный не кастятся обратно до Date, т.к. хранятся в формате FIRTimestamp.
Вопросы:

  1. Как правильно извлечь FIRTimestamp и прокастить до Date.
  2. По-скольку в Firestore значения FIRTimestamp храняться в микросекундах и допольнительная инфа обрубается (если я правильно понял), не проще ли перед сохранением отформатить дату в стринг Dateformatter’om, а потом извлечь тем же макаром? (Я так как-то делал с сохранением данных дат в файл, когда остались только поля со String и не было core data, и замечательно работало :smile: )

#4

Разговор сам с собой продолжается ))))

Кому-то точно пригодится:

при извлечении из базы Firestore параметры формата FIRTimestamp (любую сохранённую дату в формате Date) кастим до Timestamp. Далее у этого параметра (переменной) вызываем свойство .dateValue() и получаем формат нормальной даты.
Пример на всякий случай:
обращаемся к подколлекции “tasks” в документе, который лежит в корневой коллекции “users”. Получеам snapshot. Снапшот раскладываем на “карту” и получаем массив словарей. Массив прогоняем через цикл и создаём наш экземпляр структуры Task через базовую инициализацию (почему-то через вычисляемое свойство dictionary именно Task так и не заработало) с ньюансами, выше описанными (Timestamp и .dateValue()). Добавляем в наш, до этого созданный массив элементов Task. Enjoy. (Наверняка есть более элегантный способ, чем этот - посдкажите :wink: )

Firestore.firestore().collection("users").document((currentUser?.uid)!).collection("tasks").getDocuments { (querySnapshot, error) in
            if let error = error {
                ...
            } else {
                let result = querySnapshot?.documents.map({$0.data()})
                for x in result! {
                    let xTask = Task(title: x["title"] as! String, text: x["text"] as! String, completed: x["completed"] as! Bool, startDate: (x["startDate"] as! Timestamp).dateValue(), endDate: (x["endDate"] as! Timestamp).dateValue(), creatorID: x["creatorID"] as! String)
                    self.tasks.append(xTask)
                   
                }

            }
        }

З.Ы. непонятно пока как там с сохранением timezone и прочего и всё же

Да и тут коряво сделано создание экзепляра класса, естесственно можно при парсе через обычный .docements() использовать обычный инициализатор dictionary, который у всех точно есть при изучении firebase ))

UPD.
Позже я всё же изменил код без использования map - так и не понял сокральный смысл его использования:

 guard let snapshot = snapshot else {return}
            self.tasks = []
            for x in snapshot.documents {
                let xTask = Task(id: x.documentID, title: x["title"] as? String ?? "", text: x["text"] as? String ?? "", completed: x["completed"] as? Bool ?? false, startDate: (x["startDate"] as? Timestamp)?.dateValue() ?? Date(), endDate: (x["endDate"] as? Timestamp)?.dateValue() ?? Date(), creatorID: x["creatorID"] as? String ?? "", creatorName: x["creatorName"] as? String ?? "")
                self.tasks.append(xTask)
            }
}

#5

С сохранеием timeZone всё нормально - дата хранится как надо и нечего придумавыть лишнего не надо :slight_smile:

Опять же необязательно раскладвывать на map - при обращении к .documents() всё нормально преобразуется и без map. Я точно пока понимаю, что не понимаю тонкостей мэп и как с ним лучше работать )))


#6

Кто-нибудь работал с типом данных Reference? Где лучше применять? Как извлекать и как кастить?

Очень мало инфы по работе с этим типом данный в FireStore даже на английском.


#7

В моем проектике Reference – это ссылка на документ в другой коллекции, например. Это удобно, когда есть несколько документов, которые должны хранить одинаковые данные. С помощью Reference в будущем можно будет изменить что-то в одном месте, а не в 10 других.

Работаю я с ними так – сначала каст до Any, в методе вызова каст до DocumentReference, обращаюсь к свойству path и делаю запрос.

init(object: [String: Any]?) {
    self.abilities = object["abilities"] as? [Any]
    self.achievements = object["achievements"] as? [Any]
}

func getDocument(from reference: Any) {
    let castedReference = reference as! DocumentReference
    let path = castedReference.path
    
    Firestore.firestore().document(path).getDocument {}
}

Не претендую на правильность исполнения, но во всяком случае это работает :smile:


#8

Так вот главный вопрос: лучше использовать ссылку на документ или просто хранить ID в свойствах и по нему обращать?


#9

Если я вас правильно понял, то ссылка и ID, в принципе, – одно и то же. Разве что ссылка выглядит информативней (в моем случае) и

You can use references in queries like any other value: for filtering, ordering, and for paging (startAt/startAfter).

С ID такого вроде проделать не получится.


#10

конечно получится, ведь ID храню как String в одном из свойств документа. Вот и не пойму зачем reference - ведь должен же быть какой-то высший смысл ))


#11

Очередной вопрос - нужна помощь опытных по работе с map:
например
42
в Firestore словари храняться в map (ключ-значение). Данные записываются без проблем при помощи метода .updateData([String : Any]) по нужному полю. По определённому полю у меня хранится словарь (в firestore это map) - получается общая структура типа:
[String : [String : Any]]. Вроде всё просто, ведь когда мы в словарь пишем по новому ключу, то данные добавляются. Тут же получается, что используя метод .updateData() с новым ключём данные перезаписываются, т.е. всместо двух ключей и новых данных я получаю словарь с одним (новым) ключём и старый перезаписывается!

Например: у меня уже не пустое поле childTask - есть один ключ со значением. Используя метод .updateData() я создаю НОВЫЙ словарь по этому полю childTask - т.е. перезаписываю страрый. Как же не пересобирая (читая предыдущий словарь и добавляя старые элементы в новый) словарь добавить один элемент?

З.Ы в официальной документации вообще не нашёл принципов работы с map…


#12

РЕШЕНО.
Как обычно просмотрел в примерах офф доков похожий случай. Доступ к ключам вложенных словарей не совсем очевиден - через ТОЧКУ в кавычках )):

.updateData(["firstLevelKey.secondLevelKey" : newValue]