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!
Related
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()
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.
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
}
}
I'm using this example: QML Flipable Example.
I created rectangles with GridLayout. I just added my new state the example:
states: [
State {
name: 'back'
PropertyChanges { target: rotation; angle: 180 }
when: flipable.flipped
},
State {
name: 'remove'
PropertyChanges {
target: card
visible: false
}
}
]
I wanted when I click the rectangles, check rectangles, they open and if they are same or not. My algorithm for this job:
property int card1: -1
property int card2: -1
property int remaining: 20
function cardClicked(index) {
var card = repeater.itemAt(index); // Selected card
if (!card.flipped) { // If selected card is not opened
card.flipped = true; // Open card
if (card1 === -1) { // If card is first selected card
card1 = index; // Set first selected card
} else { // If selected card is not first card, I mean that is second card because first card is already selected
card2 = index; // Set second card
area.enabled = false; // Disabled GridLayout (area)
delayTimer.start(); // Start `Timer` QML component
}
} else { // If card is opened so, close that card, because that card is opened and player clicked its
card.flipped = false; // Close that card
card1 = -1; // first card is not selected
}
}
function validateCards() {
var state = ''; // Default state: Close cards
if (imageIndexes[card1] === imageIndexes[card2]) { // If first card and second card are equal, you found same cards :)
state = 'remove'; // You found cards so, remove cards
--remaining; // If this equals 0, you found all cards
}
// If cards are not same, close cards but if they are same remove them
repeater.itemAt(card1).state = state;
repeater.itemAt(card2).state = state;
card1 = -1; // first card is not selected
card2 = -1; // second card is not selected
area.enabled = true; // Enabled area (GridLayout)
if (remaining === 0) { // If you found all cards, game over
console.log('Game Over!');
}
}
I added MouseArea into Rectangles:
MouseArea {
anchors.fill: parent
onClicked: cardClicked(index) // This index belongs to my `Repeater`.
}
I put a Timer for animation to work correctly and check cards:
Timer {
id: delayTimer
interval: 1000
onTriggered: validateCards()
}
This example and animations are running nice but sometimes they aren't running correctly:
How can I solve this animation bug?
UPDATE!
You can find all source code on here.
I think you are complicating the application by using the timer since you do not know if the item finished changing its position, it would be appropriate to execute that task when it finishes turning the letter over.
Another error that I see in your code is that you are assigning a state = "".
I have modified in the following parts:
GameArea.qml
GridLayout {
[...]
property variant imageIndexes: GameUtils.generateCardIndexes(
imageCount, repeatCount)
property int lastIndex : -1
Repeater {
id: repeater
model: 40
Card {
id: card
backImageSource: 'qrc:/images/img_' + area.imageIndexes[index] + '.jpg'
onFinished: verify(index)
}
}
function verify(index){
if(lastIndex == -1){
lastIndex = index
return
}
area.enabled = false
var lastItem = repeater.itemAt(lastIndex)
var currentItem = repeater.itemAt(index)
if(lastItem.backImageSource === currentItem.backImageSource){
lastItem.state = "remove"
currentItem.state = "remove"
}
else{
lastItem.flipped = false
currentItem.flipped = false
}
if(repeater.model === 0){
console.log("Winning")
}
lastIndex = -1
area.enabled = true
}
}
Card.qml
Item {
[...]
property alias state: flipable.state
signal finished()
Flipable {
[...]
transitions: Transition {
NumberAnimation { target: rotation; property: 'angle'; duration: 400 }
onRunningChanged: {
if ((state == "back") && (!running))
finished()
}
}
MouseArea {
anchors.fill: parent
onClicked: card.flipped = true
}
}
}
The complete code can be found at the following link
I am working in extjs4. I have view with grid as item with code:
{
margin : '10 0 5 100',
xtype : 'grid',
id : 'g3',
//title : 'Educational Details',
store:'qb.qbquestionoptionStore',
columns : [ {
text : 'questionId',
dataIndex : 'questionId',
flex : 1
},
{
text : 'category',
dataIndex : 'category',
flex : 1
}, {
text : 'Answer',
dataIndex : 'isAnswer',
flex : 2.5
},
{
header : 'Remove',
renderer : function(val) {
return 'Remove';
},
}
So on clicking on remove link,corresponding entry gets deleted from database. But grid is still showing that deleted entry. In controller I have code for it as-
deleterow:function(cmp)
{
cmp.mon(cmp.getEl(),'click',function(event,target)
{
if(target.id=='remove')
{
// alert("hello");
listview=Ext.getCmp('g3');
listview.on({
itemClick: function(dv, record, item, index, e,opts)
{
liststore=this.getStore('qb.qbquestioncomplexityStore').sync();
liststore.load({
params:{
id:record.data.id,
questionId:record.data.questionId
}
});
console.log(record);
console.log(" Id is "+record.data.id);
var shopCart=Ext.create('Balaee.model.qb.qbquestioncomplexityModel',
{
id:record.data.id,
questionId:record.data.questionId
});
Ext.Msg.confirm('Confirm to delete', 'Want to delete record?', function (button)
{
if (button == 'yes')
{
shopCart.destroy();
}
}); }
}); }
},this,{delegate:"a"});
},
So how to delete record from grid?.
to remove a row from gridpanel, i do something like this:
var selectedRecord = grid.getSelectionModel().getSelection()[0];
grid.getStore().each(function(rec) {
if (rec == selectedRecord) {
grid.store.remove(rec);
}
});
grid.getView().refresh();
Your code is a bit weird but I think you are nearly there:
The easiest way is when you have set a proxy to the model. You just need to call destroy(). Any stores this record is bound to will be notified.
if (button == 'yes'){
record.destroy();
shopCart.destroy();
}
If not I assume for this example that your record is only bound to one store, then you can do it like
if (button == 'yes'){
var s = record.store;
s.remove(record);
s.store.sync();
shopCart.destroy();
}