Read/Callback multiple firebase values from query in Qt - qt

I'm trying to read all children of part of my database from one command, so I can update Firebase and it will automatically display in my app as the various titles.
the part of my database that I want to read is as follows:
public
bigqueryobject
title1
title2
title3
title4
I am working in Qt and have tried different combinations using orderByKey, orderByChild and orderByValue with the following code:
firebaseDb.getValue("public/bigqueryobject",{
orderByKeys: true
}, function(success, key, value) {
if(success) {
console.debug("Read user value for key", key, "fromFBDB: ", value);
myArray.push(value); combobox.model = myArray
}
})
when doing the above my log states:
"Read user value for key bigqueryobject fromFBDB: [object Object]
Read Value [object Object] for keybigqueryobject"
yet no responses are displayed, what could be the issue here?!?

So after previously trying to push the read value to an array to add to my combobox I was only getting one dropdown option with all read values in one row; simply removing the array worked perfectly, code below!
onFirebaseReady: {
firebaseDb.getValue("locationsDepartments/locations", {
orderByValue: true
}, function(success, key, value) {
if(success) {
combobox.model = value
}
})
}
Quick2.ComboBox {
id: combobox
model: []
delegate: Quick2.ItemDelegate {
width: combobox.width
height: combobox.height
contentItem: AppText {
text: modelData
}
highlighted: combobox.highlightedIndex == index
}
contentItem: AppText {
width: combobox.width - combobox.indicator.width - combobox2.spacing
text: combobox.displayText
wrapMode: Text.NoWrap
}
}

Related

How to refresh view with fetched data - Firestore & SwiftUI

Short: The Images in my view are not updating after the first load. The URL remains the same as the previous loaded view, however the rest of the view that doesn't fetch a URL or data from storage is updated.
Full: I have two Views, a ListView and a DetailView.
In the ListView I display a list of type List. The detail view is supposed to show each Profile from List.profiles. I do this by storing each string uid in List.profiles and calling model.fetchProfiles to fetch the profiles for each list selected.
On the first selected List model.fetchProfiles returns the documents and model.profiles displays the data fine in the DetailView.
When first loading the DetailView the ProfileRow on appear is called and logs the profiles fetched. Then the ProfileRow loads the imageURL from the imagePath and uses it like to fetch the image.
Console: Load List1
CARD DID APPEAR: Profiles []
CARD DID APPEAR: SortedProfiles []
CARD ROW
CARD ROW DID APPEAR: Profiles profiles/XXXXXX/Profile/profile.png
CARD ROW DID APPEAR: SortedProfiles profiles/XXXXXX/Profile/profile.png
Get url from image path: profiles/XXXXXX/Profile/profile.png
Image URL: https://firebasestorage.googleapis.com/APPNAME/profiles%XXXXXXX
When selecting the second List from ListView the ProfileRow didAppear is not called due to;
if model.profiles.count > 0 {
print("CARD ROW DID APPEAR: Profiles \(model.profiles[0]. imgPath)")
print("CARD ROW DID APPEAR: Sorted \(model.sortedProfiles[0].imgPath)")
}
and won't ever again when selecting a List in ListView, however the rest of the profile data in the ProfileRow is displayed such as name so the data must be fetched.
The ImagePath is the same as the first view loading the exact same image. All other properties for the Profile such as name are loaded correctly.
Console: Load List2
CARD DID APPEAR: Profiles []
CARD DID APPEAR: SortedProfiles []
CARD ROW
Get url from image path: profiles/XXXXXX/Profile/profile.png
Image URL:
https://firebasestorage.googleapis.com/APPNAME/profiles%XXXXXXX
If I then navigate to List1 then the image for List2 appears, if I reselect List2 the image appears fine. The image show is correct on first load, and when selecting another list it always the one from before.
Can anyone help me out ?
First View
struct ListViw: View {
#EnvironmentObject var model: Model
var body: some View {
VStack {
ForEach(model.lists.indices, id: \.self) { index in
NavigationLink(
destination: DetailView()
.environmentObject(model)
.onAppear() {
model.fetchProfiles()
}
) {
ListRow(home:model.lists[index])
.environmentObject(model)
}
.isDetailLink(false)
}
}
}
}
DetailView Card
struct ProfilesCard: View {
#EnvironmentObject var model: Model
var body: some View {
VStack(alignment: .trailing, spacing: 16) {
if !model.sortedProfiles.isEmpty {
VStack(alignment: .leading, spacing: 16) {
ForEach(model.sortedProfiles.indices, id: \.self) { index in
ProfileRow(
name: "\(model.sortedProfiles[index].firstName) \(model.sortedProfiles[index].lastName)",
imgPath: model.sortedProfiles[index].imgPath,
index: index)
.environmentObject(model)
}
}
.padding(.top, 16)
}
}//End of Card
.modifier(Card())
.onAppear() {
print("CARD DID APPEAR: Profiles \(model.profiles)")
print("CARD DID APPEAR: SORTED \(model.sortedTenants)")
}
}
}
struct ProfileRow: View {
#EnvironmentObject var model: Model
#State var imageURL = URL(string: "")
var name: String
var imgPath: String
var index: Int
private func loadImage() {
print("load image: \(imgPath)")
DispatchQueue.main.async {
fm.getURLFromFirestore(path: imgPath, success: { (imgURL) in
print("Image URL: \(imgURL)")
imageURL = imgURL
}) { (error) in
print(error)
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .center, spacing: 12) {
KFImage(imageURL,options: [.transition(.fade(0.2)), .forceRefresh])
.placeholder {
Rectangle().foregroundColor(.gray)
}
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 32, height: 32)
.cornerRadius(16)
// Profile text is always displayed correctly
Text(name)
.modifier(BodyText())
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.onAppear() {
print("CARD ROW")
// Crashes if check is not there
if model.profiles.count > 0 {
print("CARD ROW DID APPEAR: Profiles \(model.profiles[0]. imgPath)")
print("CARD ROW DID APPEAR: Sorted \(model.sortedProfiles[0].imgPath)")
}
loadImage()
}
}
}
Model
class Model: ObservableObject {
init() {
fetchData()
}
#Published var profiles: [Profile] = []
var sortedProfiles: [Profile] {return profiles.removeDuplicates }
#Published var list: List? {
didSet {
fetchProfiles()
}
}
func fetchData() {
if let currentUser = Auth.auth().currentUser {
email = currentUser.email!
db.collection("lists")
.whereField("createdBy", isEqualTo: currentUser.uid)
.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
return
}
self.lists = documents.compactMap { queryDocumentSnapshot -> List? in
return try? queryDocumentSnapshot.data(as: List.self)
}
}
}
}
func fetchProfiles() {
profiles.removeAll()
for p in list!.profiles {
firestoreManager.fetchProfile(uid: t, completion: { [self] profile in
profiles.append(profile)
})
}
}
}
Update
What I have tried so far is to use didSet for the ImgPath or ImgURL but still not luck. Also have tried using model.profiles directly.
In all callbacks with Firestore API make assignment for published or state properties on main queue, because callback might be called on background queue.
So, assuming data is returned and parsed correctly, here is as it should look like
for p in list!.profiles {
firestoreManager.fetchProfile(uid: t, completion: { [self] profile in
DispatchQueue.main.async {
profiles.append(profile)
}
})
}
also I would recommend to avoid same naming for your custom types with SDK types - there might be very confusing non-obvious errors
// List model below might conflict with SwiftUI List
return try? queryDocumentSnapshot.data(as: List.self)
As per my knowledge its not the problem from firebase end, because the ones data fetched the new data is updated. You are facing problem of image caching. Caching is a technique that stores a copy of a given resource. So when the image is loaded for first time it get cached and whenever you are reloading images are displayed from cache instead of loading from URL. This is done for more network usage.
You can programatically clear cache by adding following code before your image loading.
Alamofire uses NSURLCache in the background so you just have to call:
NSURLCache.sharedURLCache().removeAllCachedResponses()
Update for Swift 4.1
URLCache.shared.removeAllCachedResponses()

Can I Replace SimpleRow with Image if no modelData avaliable

I have a ListView, where the currently displayed modelData changes as a button cycles through several department options. If one of these departments has no data, my delegate continues showing the previous list data until it reaches a new section of modelData with data.
What I want to do, is when the model is 'empty' (being undefined, which may happen when the key it's looking for is yet to be created in my Firebase Database, or no items are currently visible), text/image is shown instead; i.e, "Move along now, nothing to see here".
My model is drawn from JSON, an example is below. and my calendarUserItems is the root node of multiple children within my Firebase Database, the aim of my AppButton.groupCycle was too add a further direction to each child node, filtering the data by this to view and edit within the page.
A sample of my code is:
Page {
id: adminPage
property var departments: [1,2,3,4]
property int currGroupIndex: 0
AppButton {
id: groupCycle
text: "Viewing: " + departments[currGroupIndex]
onClicked: {
if (currGroupIndex == departments.length - 1)
currGroupIndex = 0;
else
currGroupIndex++;
}
}
ListView {
model: Object.keys(dataModel.calendarUserItems[departments[currGroupIndex]])
delegate: modelData.visible ? currentGroupList : emptyHol
Component {
id: emptyHol
AppText {
text: "nothing to see here move along now!"
}
}
Component {
id: currentGroupList
SimpleRow {
id: container
readonly property var calendarUserItem: dataModel.calendarUserItems[departments[currGroupIndex]][modelData] || {}
visible: container.calendarUserItem.status === "pending" ? true : false
// only pending items visible
// remaining code for simple row
}
}
}
}
an example of JSON within my dataModel.calendarUserItems is:
"groupName": [
{ "department1":
{ "1555111624727" : {
"creationDate" : 1555111624727,
"date" : "2019-03-15T12:00:00.000",
"name" : "Edward Lawrence",
"status": "pending"
},
//several of these entries within department1
},
},
{ "department2":
{ "1555111624727" : {
"creationDate" : 1555111624456,
"date" : "2019-05-1T12:00:00.000",
"name" : "Katie P",
"status": 1
},
//several of these entries within department2
},
}
//departments 3 & 4 as the same
]
If departments 2 and 3 have modelData, yet 1 and 4 do not, I want the text to display instead, and the ListView emptied, instead of showing the previous modelData.
I have tried playing with the image/text visibility but the issue lays more with clearing the modelData and I'm unsure where to begin?
Any help is greatly appreciated!
I have achieved the display by using the following as my delegate:
delegate: {
if (!(departments[currGroupIndex] in dataModel.calendarUserItems) ) {
return emptyHol;
}
var subgroups = Object.keys(dataModel.calendarUserItems[departments[currGroupIndex]]);
for (var i in subgroups) {
var subgroup = dataModel.calendarUserItems[departments[currGroupIndex]][subgroups[i]];
modelArr.push(subgroup);
}
var modelObect = modelArr.find( function(obj) { return obj.status === "pending"; } );
if (modelObect === undefined) {
return emptyHol;
}
return currentGroupList;
}
Then when my AppButton.groupCycle is pressed, I have added modelArr = [] to clear the array on each press, this works as intended.
Thanks!

QT Quick Test KeyClicks

I am trying to implement a keyClick with a shift modifier but it doesn't work. Below is a basic setup of what I am trying to do. The first test_case1 can perform what I am doing but I'd like the second test_case2 to work as well but with using the Qt.ShiftModifier.
../MyTextBox.qml
Page {
id: page1
objectName: "page1"
TextField {
id: lastNameField
objectName: "lastNameField"
text: qsTr("")
}
}
tst_page.qml
import "../"
Item {
width: 800
height: 600
MyTextBox {
id: page1
}
TestCase {
id: "txtBox"
when: windowShown
function test_case1 () {
//var qmlObj = findChild(page1, "lastNameField")
var qmlObj = page1.lastNameField
// Bring to focus
mouseClick(qmlObj, Qt.LeftButton, Qt.NoModifier)
// Keypress
keyPress("Y")
keyPress("e")
keyPress("s")
tryCompare(qmlObj, "text", "Yes") // pass
}
function test_case2 () {
var qmlObj = page1.lastNameField
// Bring to focus
mouseClick(qmlObj, Qt.LeftButton, Qt.NoModifier)
// Keypress
keyClick(QT.Key_Y, Qt.ShiftModifier)
keyClick(QT.Key_E)
keyClick(QT.Key_S)
tryCompare(qmlObj, "text", "Yes") // fail
}
}
}
test output
PASS : test_case1()
FAIL! : test_case2()
Actual (): yes
Expected (): Yes
Edit: Added a simple project to github for testing.

Add string of dates to qml calendar

UPDATED AFTER ADJUSTMENTS
So after amending my code from the suggestions and working with the visibility of my image and pushing the dates to an array I am still not seeing the individually marked dates, my new code is:
property var arrayFromFirebase: []
onFirebaseReady: {
firebaseDb.getUserValue("dates", {
orderByChild: true
}, function(success, key, value) {
if(success) {
console.log(JSON.stringify(value))
for( date in value){arrayFromFirebase.push(date.date)}
}
})
}
Image {
visible: arrayFromFirebase.indexOf(styleData.date.getDate()) > -1
}
My log still reads the arrayFromFirebase in the format of:
[{"date":"2018-10-01T21:17:00.926"},{"date":"2018-10-02T12:00:00.000"},{"date":"2018-10-03T12:00:00.000"},{"date":"2018-10-06T12:00:00.000"},{"date":"2018-10-07T12:00:00.000"},{"date":"2018-10-08T12:00:00.000"}]
ORIGINAL QUESTION
I have my a calendar which my users save selected dates which are stored in firebase, my calendar is built with example in the link
QtQuick Calendar
My user dates are stored to firebase, and when i read them i can receive them in the log using JSON.stringify,
When writing the saved dates to my database I use the following code:
property var userData: {
"selectedDates": [
{ },
]
}
AppButton {
id: saveButton
text: "Save & Request"
anchors.right: parent.right
textColor: "#4e4e4e"
backgroundColor: "#d1d1d1"
onClicked: {
userData.selectedDates.push({ "date": calendar.selectedDate});
console.log(JSON.stringify(userData));
firebaseDb.setUserValue("dates", userData.selectedDates)
}
}
Then when reading the dates I use the following:
FirebaseDatabase {
config: customConfig
onFirebaseReady: {
firebaseDb.getUserValue("dates", {
startAt: {
Key: "1/date",
}
}, function(success, key, value) {
if(success) {
console.log(JSON.stringify(value))
}
})
}
}
Once this is read the log displays the dates as follows:
[{"date":"2018-10-01T21:17:00.926"},{"date":"2018-10-02T12:00:00.000"},{"date":"2018-10-03T12:00:00.000"},{"date":"2018-10-06T12:00:00.000"},{"date":"2018-10-07T12:00:00.000"},{"date":"2018-10-08T12:00:00.000"}]
I want to take this string of dates and add markers to my calendar to show they are booked?
How would I go about this?
The Type FirebaseDatabase is documented here
CALENDAR QML CODE
CalendarQMLCode
UPDATE (after #derM comment)
No need to use sqlite DB model
Just in the dayDelegate we play with the event indicator image visibility
For that after getting your data from firebase, save it to an array (we name it arrayFromFireBase) and check if styleData.date is in that array
FirebaseDatabase {
config: customConfig
onFirebaseReady: {
firebaseDb.getUserValue("dates", {
startAt: {
Key: "1/date",
}
}, function(success, key, value) {
if(success) {
console.log(JSON.stringify(value))
// value here is JSON ARRAY
// value =[{"date":"2018-10-01T21:17:00.926"},{"date":"2018-10-02T12:00:00.000"}]
// so value[0].date ="2018-10-01T21:17:00.926"
//then you gonna insert here your dates in arrayFromFireBase as strings as follow
for(i=0 ; i < arrayFromFireBase.length-1 ; i++)
arrayFromFireBase.push(new Date(value[i].date).getTime())
}
})
}
}
Calendar {
...
style: CalendarStyle {
dayDelegate: Item {
...
Image {
// HERE WE MUST COMPARE TIME (we used Date.getTime()) FROM FIREBASE with the calendar's date converted to time also
visible: arrayFromFireBase.indexOf(styleData.date.getTime()) > -1
...
source: "qrc:/images/eventindicator.png"
}
}
}
}
OLD ANSWER
Part of the code was taken from Qt Quick Controls - Calendar Example
Just added void SqlEventModel::insertDates(QStringList dates)
C++ part
SqlEventModel::SqlEventModel()
{
createConnection();
}
void SqlEventModel::createConnection()
{
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(":memory:");
if (!db.open()) {
qFatal("Cannot open database");
return;
}
}
void SqlEventModel::insertDates(QStringList dates){
QSqlQuery query;
// We store the time as seconds because it's easier to query.
// It depends on what you want as columns for your events
query.exec("create table Event (name TEXT, startDate DATE, startTime INT,
endDate DATE, endTime INT)");
foreach(QString date in dates){
// It depends on what you want as columns for your events
query.exec(QString("insert into Event values('%1', '%2',
%3, '%4', %5)").arg(date[0]).arg(date[1]).arg(date[2]).arg(date[3]).arg(date[4]));
}
return;
}
QML part
SqlEventModel {
id: eventModel
}
FirebaseDatabase {
config: customConfig
onFirebaseReady: {
firebaseDb.getUserValue("dates", {
startAt: {
Key: "1/date",
}
}, function(success, key, value) {
if(success) {
console.log(JSON.stringify(value))
//Insert here your dates in sql model
eventModel.insertDates(value)
}
})
}
}
So, after a development holiday, I have a working solution! So see below (excluding all excess code) and thanks to everyone for your help!! (especially #Redanium for your patience)
Main.qml:
App {
property var arrayFromFireBase: []
onLoggedIn: {
firebaseDb.getUserValue("dates/selectedDates", {},
function(success, key, value) {
if(success) {
for(var idx in value)
arrayFromFireBase.push(new Date(value[idx].date).getTime())
})}}
Then on my calendar page the following:
CalendarPage.qml:
Page {
AppButton {
id: saveButton
text: "Save & Request"
onClicked: {
userData.selectedDates.push({"date": calendar.selectedDate});
console.log(JSON.stringify(userData));
firebaseDb.setUserValue("dates", userData)
}
}
property var userData: {
"selectedDates": [{}]
}
Image {
visible: arrayFromFireBase.indexOf(styleData.date.getTime()) > -1
}
}

QML conditional Binding not working as expected

If I have a simple Binding object of the form:
Rectangle {
height: 400
width: 500
property var someObj: null
Binding on color {
when: someObj
value: someObj.color
}
}
Then I would expect that when someObj is not null, someObj's color property is bound to this object's color property. What I actually get is a runtime error message:
TypeError: Cannot read property 'color' of null
Any reason why this doesn't work?
Doing the almost equivalent JavaScript expression:
color: {
if ( someObj != null ) {
return someObj.color;
} else {
return "black";
}
}
Works as expected.
As mentioned in the comment by BaCaRoZzo, the minimal example given in the question does not work because it gives a ReferenceError: someObj is not defined. However, after fixing this with an id for the Rectangle, then the example actually works despite the TypeError:
Rectangle {
id: rect
height: 400
width: 500
property var someObj: null
Binding on color {
when: rect.someObj
value: rect.someObj.color
}
}
This correctly sets the color as expected when rect.someObj is set and contains a color property.
The reason for the TypeError is that the expression rect.someObj.color is evaluated already when the Binding is created (see QTBUG-22005).
So to prevent the TypeError, one can simply check for rect.someObj to be set in the value expression of the Binding:
Binding on color {
when: rect.someObj
value: rect.someObj ? rect.someObj.color : undefined
}
I would do it in the following way:
import QtQuick 2.0
import QtQuick.Controls 1.4
Rectangle {
height: 400
width: 500
property var someObj
color: someObj ? someObj.color : "black"
Button {
id: buttonTest
text: "test"
onClicked: parent.someObj = test
}
Button {
id: buttonTest2
anchors.left: buttonTest.right
text: "test2"
onClicked: parent.someObj = test2
}
QtObject {
id: test
property color color: "red"
}
QtObject {
id: test2
property color color: "blue"
}
}
If someObj is undefined the color of the rectangle is black, if someObj is defined, the Value of the color property is chosen.
Edit: I've seen to late, that that's only what mlvljr suggested in the comments, sorry.
The QML syntax defines that curly braces on the right-hand-side of a property value initialization assignment denote a binding assignment. This can be confusing when initializing a var property, as empty curly braces in JavaScript can denote either an expression block or an empty object declaration. If you wish to initialize a var property to an empty object value, you should wrap the curly braces in parentheses.
For example:
Item {
property var first: {} // nothing = undefined
property var second: {{}} // empty expression block = undefined
property var third: ({}) // empty object
}
In the previous example, the first property is bound to an empty expression, whose result is undefined. The second property is bound to an expression which contains a single, empty expression block ("{}"), which similarly has an undefined result. The third property is bound to an expression which is evaluated as an empty object declaration, and thus the property will be initialized with that empty object value.
Similarly, a colon in JavaScript can be either an object property value assignment, or a code label. Thus, initializing a var property with an object declaration can also require parentheses:
Item {
property var first: { example: 'true' } // example is interpreted as a label
property var second: ({ example: 'true' }) // example is interpreted as a property
property var third: { 'example': 'true' } // example is interpreted as a property
Component.onCompleted: {
console.log(first.example) // prints 'undefined', as "first" was assigned a string
console.log(second.example) // prints 'true'
console.log(third.example) // prints 'true'
}
}
So the code should be as follow:
Rectangle {
height: 400
width: 500
property var someObj: ({color: ''})
Binding on color {
when: someObj.color
value: someObj.color
}
}

Resources