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
}
}
Related
My app loads a simple collection of .ics URLs from Firebase. I then download and parse each calendar file to save in an array of structs.
This code works:
struct Calendar: Identifiable, Codable {
#DocumentID var id: String?
var urlString: String = ""
var name: String = ""
var licenseNumber: String = ""
}
struct ScheduleView: View {
#FirestoreQuery(collectionPath: "calendars") var calendars: [Calendar]
#State var matches: [Match] = []
var body: some View {
NavigationView {
VStack {
List(calendars) { calendar in
Text (calendar.urlString)
}
List(matches) { match in
Text (match.dateString)
}
Spacer()
Button {
downloadMatchCalendars()
} label: {
Text ("Download calendars")
}
}
}
}
func downloadMatchCalendars() { /* code that correctly populates matches */ }
}
List(calendars) is immediately visible when I launch and it updates when the Firebase collection changes. But, I can't get List(matches) to update, too. It's blank until I click the button. And if a new calendar is added in Firebase, the new matches are not loaded/displayed.
I tried using NavigationView{}.onAppear(perform: dowloadMatchCalendars). I tried a didSet on my #FirestoreQuery. Neither worked.
How can I run downloadMatchCalendars after loading the calendars from Firebase.
(See next post for my second related question.)
I have data in a dictionary which is [String: String]. What I want to provide is an interface to the user to edit the values in the dictionary, while the keys remain fixed. I can see how to display the values, but putting them into a TextField is what I want, and haven't been able to find how to do.
Here is the code:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.sorted(by: <), id: \.key) { key, value in
HStack {
Text(key)
TextField("", text: $entries[key])
}
}
}
}
}
This doesn't compile, with no fewer than three errors on the TextField line:
Cannot convert value of type 'Slice<Binding<[String : String]>>' to expected argument type 'Binding'
Cannot convert value of type 'String' to expected argument type 'Range<Binding<[String : String]>.Index>'
Referencing subscript 'subscript(_:)' on 'Binding' requires that '[String : String]' conform to 'MutableCollection'
So obviously I am doing things incorrectly, but I am lost trying to find what the correct way would be, and haven't been able to find an answer in an internet search. Can anyone point me in the right direction?
you could try this simple approach:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.keys.sorted(by: <), id: \.self) { key in
HStack {
Text(key)
TextField("", text: Binding(
get: { entries[key]! },
set: { entries[key] = $0 }
))
}
}
}
}
}
struct ContentView: View {
#State var entries: [String: String] = ["key1":"val1", "key2":"val2", "key3":"val3", "key4":"val4"]
var body: some View {
dictionaryEditor(entries: $entries)
Button(action: { print("----> entries: \(entries)") }) {
Text("print entries")
}
}
}
The problem is that entries[key] returns an optional String value while the text parameter of TextField expects a Binding of non optional String.
You can create an optional binding extension and then you can use it safely:
extension Binding where Value == String? {
var optionalBind: Binding<String> {
.init(
get: {
wrappedValue ?? ""
}, set: {
wrappedValue = $0
}
)
}
}
Then you can just add the optionalBind to your code:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.sorted(by: <), id: \.key) { key, value in
HStack {
Text(key)
TextField("", text: $entries[key].optionalBind) // <--
}
}
}
}
}
I am creating a Form in SwiftUi with a section that is including a flexible number of instruction.
Next to the last instruction TextField, I am showing a "+"-Button that is extending the instructions array with a new member:
var body: some View {
NavigationView {
Form {
...
Section(header: Text("Instructions")) {
InstructionsSectionView(instructions: $recipeViewModel.recipe.instructions)
}
...
struct InstructionsSectionView: View {
#Binding var instructions: [String]
var body: some View {
ForEach(instructions.indices, id: \.self) { index in
HStack {
TextField("Instruction", text: $instructions[index])
if(index == instructions.count-1) {
addInstructionButton
}
}
}
}
var addInstructionButton: some View {
Button(action: {
instructions.append("")
}) {
Image(systemName: "plus.circle.fill")
}
}
}
Now the problem is, that the button click-area is not limited to the picture but to the whole last row. Precisely the part just around the textField, meaning if I click in it, I can edit the text, but if I click on the border somewhere, a new entry is added.
I assume that this is specific to Form {} (or also List{}), since it does not happen if I use a Button next to a text field in a "normal" set-up.
Is there something wrong with my code? Is this an expected behaviour?
I am not sure why border is getting tappable, but as a workaround I used plainButtonStyle and that seems to fix this issue, and keeps functionality intact .
struct TestView: View {
#State private var endAmount: CGFloat = 0
#State private var recipeViewModel = ["abc","Deef"]
var body: some View {
NavigationView {
Form {
Section(header: Text("Instructions")) {
InstructionsSectionView(instructions: $recipeViewModel)
}
}
}
}
}
struct InstructionsSectionView: View {
#Binding var instructions: [String]
var body: some View {
ForEach(instructions.indices, id: \.self) { index in
HStack {
TextField("Instruction", text: $instructions[index])
Spacer()
if(index == instructions.count-1) {
addInstructionButton
.buttonStyle(PlainButtonStyle())
.foregroundColor(.blue)
}
}
}
}
var addInstructionButton: some View {
Button(action: {
instructions.append("")
}) {
Image(systemName: "plus.circle.fill")
}
}
}
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 using normalizr to organize my redux-store state.
Let's say that I have normalized todo-list:
{
result: [1, 2],
entities: {
todo: {
1: {
id: 1,
title: 'Do something'
},
2: {
id: 2,
title: 'Second todo'
}
}
}
}
Then I would like to implement addTodo action. I need to have an id in todo object, so I generate a random one:
function todoReducer(state, action) {
if(action.type == ADD_TODO) {
const todoId = generateUUID();
return {
result: [...state.result, todoId],
enitities: {
todos: {
...state.entities.todos,
[todoId]: action.todo
}
}
}
}
//...other handlers...
return state;
}
But the problem is that eventually all data will be saved to server and generated id should be replaced with real server-assigned id. Now I merge them like this:
//somewhere in reducer...
if(action.type === REPLACE_TODO) {
// copy todos map, add new entity, remove old
const todos = {
...state.entities.todos
[action.todo.id]: action.todo
};
delete todos[action.oldId];
// update results array as well
const result = state.result.filter(id => id !== oldId).concat(action.todo.id);
// return new state
return {entities: {todos}, result};
}
It seems to be a working solution, but there also a lot of overhead. Do you know any way to simplify this and don't make REPLACE_TODO operation?