List not re-loading from firestore in base view on dismiss of .sheet in SwiftUI - swift

I have another question.
I have a demo app where I add a ToDo in a Firestore database. From the base View I open a .sheet with a TextEditor where I enter data and save it into Firestore database. But on dismiss the List of ToDos in the base View is gone and is not refreshed until I go to another tab in the app and return back.
I have a ViewModel where I use a Firebase snapshot listener.
Code of the base View:
import Firebase
import Foundation
import SwiftUI
import FirebaseStorage
struct HomeMenuView: View {
#ObservedObject var toDosViewModel = ToDosViewModel()
#Binding var showAddToDoView: Bool
#State private var showModifyToDoView = false
#State private var note = ""
#State private var selectedToDoId = ""
func removeRow(at offset:IndexSet) {
for index in offset {
toDosViewModel.deleteNote(noteToDelete: toDosViewModel.todos[index].id!)
}
}
var body: some View {
ZStack{
VStack (alignment: .center){
List() {
ForEach(toDosViewModel.todos) { todo in
VStack(alignment: .leading, spacing: 10) {
Text(todo.notes)
.font(.subheadline)
.foregroundColor(Color.tabBarColor)
.lineLimit(2)
.onTapGesture {
showAddToDoView = true
selectedToDoId = todo.id!
note = todo.notes
}
}
.listRowSeparatorTint(Color.tabBarColor)
}
.onDelete(perform: removeRow)
}
.listStyle(InsetGroupedListStyle())
.onAppear() {
toDosViewModel.subscribe()
}
}
}
.sheet(isPresented: $showAddToDoView) {
VStack() {
HStack () {
Button("Save") {
guard !note.isEmpty else
{ showAddToDoView = false; return }
toDosViewModel.addNote(notes: note)
note = ""
showAddToDoView = false
}
.offset(x: 20)
Spacer()
Button("Back") {
note = ""
showAddToDoView = false
}
.offset(x: -20)
}
.frame(height: 50, alignment: .center)
TextEditor(
text: $note
)
}
}
}
}
The ViewModel:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
import UIKit
class ToDosViewModel: ObservableObject {
#Published var todos = [ToDo]()
#Published var errorMessage: String?
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
func subscribe() {
if listenerRegistration == nil {
listenerRegistration = db.collection("todos")
.order(by: "timestamp", descending: true)
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self?.todos = documents.compactMap { queryDocumentSnapshot in
let result = Result { try queryDocumentSnapshot.data(as: ToDo.self) }
switch result {
case .success(let todo):
if let todo = todo {
self?.errorMessage = nil
return todo
}
else {
self?.errorMessage = "Document doesn't exist."
return nil
}
case .failure(let error):
switch error {
case DecodingError.typeMismatch(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.valueNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.keyNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.dataCorrupted(let key):
self?.errorMessage = "\(error.localizedDescription): \(key)"
default:
self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
return nil
}
}
}
}
}
func addNote(notes: String) {
db.collection("todos").document().setData(["notes" : notes, "timestamp" : FieldValue.serverTimestamp()])
}
func modifyNote(noteToModify: String, notes: String) {
db.collection("todos").document(noteToModify).setData(["notes" : notes, "timestamp" : FieldValue.serverTimestamp()])
}
func deleteNote(noteToDelete: String) {
db.collection("todos").document(noteToDelete).delete()
}
}
Any idea what the issue could be?
Thanks a lot for your support.

Change
#ObservedObject var toDosViewModel = ToDosViewModel()
To
#StateObject var toDosViewModel = ToDosViewModel()

Related

Swiftui #binding ForEach loop with timer function not working

Hy folks, I work on a litte project for a time tracker and use Core Data for storing the values. Every timer Note has a seconds value stored that runs from inside of each timer view. I want to populate now these values to the parent view, but it's not working even when binding the note to the view. I know i have to populate the changes somehow... Can somebody help?
ContentView:
import SwiftUI
extension Int: Identifiable {
public var id: Int { self }
}
struct ContentView: View {
let coreDM: CoreDataManager
#State private var noteTitle: String = ""
#State private var notes: [Note] = [Note]() // That's the Core Data Model
private func populateNotes() {
notes = coreDM.getAllNotes()
}
var body: some View {
VStack {
if notes.count > 0 {
ForEach(0..<$notes.count,id: \.self) { i in
Text("\(notes[i].seconds)")
}
}
TextField("Enter title", text: $noteTitle)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Save") {
coreDM.saveNote(title: noteTitle, seconds: 0)
populateNotes()
}
List {
if notes.count > 0 {
ForEach(0..<$notes.count,id: \.self) { i in
NoteListView(note: $notes[i], coreDM: coreDM)
Button("Delete"){
coreDM.deleteNote(note: notes[i])
populateNotes()
}
}
}
}.listStyle(PlainListStyle())
Spacer()
}.padding()
.onAppear(perform: {
populateNotes()
})
}
}
NoteListView:
import SwiftUI
import Combine
struct NoteListView: View {
#Binding var note: Note
let coreDM: CoreDataManager
#State private var noteSeconds: Double = 0.0
#State private var noteIsRunning: Bool = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
Text(note.title ?? "")
Text("\(noteSeconds)")
.onReceive(timer) { time in
if noteIsRunning {
noteSeconds += 1
note.seconds = noteSeconds
}
}
if noteIsRunning {
Image(systemName: "pause.circle")
.resizable()
.frame(width:20, height: 20)
.onTapGesture {
withAnimation{
noteIsRunning.toggle()
note.seconds = noteSeconds
coreDM.updateNote()
}
}
}else{
Image(systemName: "record.circle")
.resizable()
.foregroundColor(.orange)
.frame(width:20, height: 20)
.onTapGesture {
withAnimation{
noteIsRunning.toggle()
}
}
}
}
.onAppear(){
noteSeconds = note.seconds
}
}
}
CoreDataManager:
import Foundation
import CoreData
class CoreDataManager {
let persistentContainer: NSPersistentContainer
init() {
persistentContainer = NSPersistentContainer(name: "TimeTrackerDataModel2")
persistentContainer.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Core Data Store failed \(error.localizedDescription)")
}
}
}
func updateNote() {
do {
try persistentContainer.viewContext.save()
} catch {
persistentContainer.viewContext.rollback()
}
}
func deleteNote(note: Note) {
persistentContainer.viewContext.delete(note)
do {
try persistentContainer.viewContext.save()
} catch {
persistentContainer.viewContext.rollback()
print("Failed to save context \(error)")
}
}
func getAllNotes() -> [Note] {
let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()
do {
return try persistentContainer.viewContext.fetch(fetchRequest)
} catch {
return []
}
}
func saveNote(title: String, seconds: Double) {
let note = Note(context: persistentContainer.viewContext)
note.title = title
note.seconds = seconds
do {
try persistentContainer.viewContext.save()
} catch {
print("Failed to save note: \(error)")
}
}
}

Update View Only After Aync Is Resolved with Completion Handler

I'm trying to update my view, only after the Async call is resolved. In the below code the arrayOfTodos.items comes in asynchronously a little after TodoListApp is rendered. The problem I'm having is that when onAppear runs, self.asyncTodoList.items is always empty since it hasn't received the values of the array yet from the network call. I'm stuck trying to figure out how to hold off on running onAppear until after the Promise is resolved, like with a completion handler?? And depending on the results of the network call, then modify the view. Thanks for any help! I've been stuck on this longer than I'll ever admit!
struct ContentView: View {
#StateObject var arrayOfTodos = AsyncGetTodosNetworkCall()
var body: some View {
TodoListApp(asyncTodoList: arrayOfTodos)
}
}
struct TodoListApp: View {
#ObservedObject var asyncTodoList: AsyncGetTodosNetworkCall
#State private var showPopUp: Bool = false
var body: some View {
NavigationView {
ZStack {
VStack {
Text("Top Area")
Text("List Area")
}
if self.showPopUp == true {
VStack {
Text("THIS IS MY POPUP!")
Text("No Items Added Yet")
}.frame(width: 300, height: 400)
}
}.onAppear {
let arrayItems = self.asyncTodoList
if arrayItems.items.isEmpty {
self.showPopUp = true
}
/*HERE! arrayItems.items.isEmpty is ALWAYS empty when onAppear
runs since it's asynchronous. What I'm trying to do is only
show the popup if the array is empty after the promise is
resolved.
What is happening is even if array resolved with multiple todos,
the popup is still showing because it was initially empty on
first run. */
}
}
}
}
class AsyncGetTodosNetworkCall: ObservableObject {
#AppStorage(DBUser.userID) var currentUserId: String?
private var REF_USERS = DB_BASE.collection(DBCOLLECTION.appUsers)
#Published var items = [TodoItem]()
func fetchTodos(toDetach: Bool) {
guard let userID = currentUserId else {
return
}
let userDoc = REF_USERS.document(String(userID))
.collection(DBCOLLECTION.todos)
.addSnapshotListener({ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No Documents Found")
return
}
self.items = documents.map { document -> TodoItem in
let todoID = document.documentID
let todoName = document.get(ToDo.todoName) as? String ?? ""
let todoCompleted = document.get(Todo.todoCompleted) as? Bool ?? false
return TodoItem(
id: todoID,
todoName: todoName,
todoCompleted: todoCompleted
)
}
})
if toDetach == true {
userDoc.remove()
}
}
}
While preparing my question, i found my own answer. Here it is in case someone down the road might run into the same issue.
struct ContentView: View {
#StateObject var arrayOfTodos = AsyncGetTodosNetworkCall()
#State var hasNoTodos: Bool = false
func getData() {
self.arrayOfTodos.fetchTodos(toDetach: false) { noTodos in
if noTodos {
self.hasNoTodos = true
}
}
}
func removeListeners() {
self.arrayOfTodos.fetchTodos(toDetach: true)
}
var body: some View {
TabView {
TodoListApp(asyncTodoList: arrayOfTodos, hasNoTodos : self.$hasNoTodos)
}.onAppear(perform: {
self.getData()
}).onDisappear(perform: {
self.removeListeners()
})
}
}
struct TodoListApp: View {
#ObservedObject var asyncTodoList: AsyncGetTodosNetworkCall
#Binding var hasNoTodos: Bool
#State private var hidePopUp: Bool = false
var body: some View {
NavigationView {
ZStack {
VStack {
Text("Top Area")
ScrollView {
LazyVStack {
ForEach(asyncTodoList.items) { item in
HStack {
Text("\(item.name)")
Spacer()
Text("Value")
}
}
}
}
}
if self.hasNoTodos == true {
if self.hidePopUp == false {
VStack {
Text("THIS IS MY POPUP!")
Text("No Items Added Yet")
}.frame(width: 300, height: 400)
}
}
}
}
}
}
class AsyncGetTodosNetworkCall: ObservableObject {
#AppStorage(DBUser.userID) var currentUserId: String?
private var REF_USERS = DB_BASE.collection(DBCOLLECTION.appUsers)
#Published var items = [TodoItem]()
func fetchTodos(toDetach: Bool, handler: #escaping (_ noTodos: Bool) -> ()) {
guard let userID = currentUserId else {
handler(true)
return
}
let userDoc = REF_USERS.document(String(userID))
.collection(DBCOLLECTION.todos)
.addSnapshotListener({ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No Documents Found")
handler(true)
return
}
self.items = documents.map { document -> TodoItem in
let todoID = document.documentID
let todoName = document.get(ToDo.todoName) as? String ?? ""
let todoCompleted = document.get(Todo.todoCompleted) as? Bool ?? false
return TodoItem(
id: todoID,
todoName: todoName,
todoCompleted: todoCompleted
)
}
handler(false)
})
if toDetach == true {
userDoc.remove()
}
}
}

Can't change #State var

I have SwiftUI view which I want to change after checking user log-pass. I'm trying to change isAuth var like you can see below.
import SwiftUI
struct Auth : View, AuthProtocol {
#State private var isAuth = false
init() {
userManager.notifier = self
}
var body : some View {
if isAuth {
WelcomeView()
} else {
VStack {
Divider()
Text("Please, wait a minute...")
Divider()
}
.frame(width: 450, height: 350)
}
}
func passAuth() {
if userManager.validateUser() {
self.isAuth.toggle()
print("isAuth: \(isAuth)")
}
}
}
And I got output isAuth: false.
I call passAuth func from this code
class <classname> {
var notifier : AuthProtocol!
func fetchUsers() {
db.collection("users").getDocuments { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
var newUser = UserData()
newUser.login = document["login"] as! String
newUser.name = document["name"] as! String
newUser.password = document["password"] as! String
newUser.accessLevel = document["access_level"] as! Int
self.usersList.append(newUser)
}
self.notifier.passAuth()
}
}
}
}
I have no idea why value of isAuth isn't changing...
We use MVVM in swiftui. so we need a viewModel. now when the network request resutl will come we can update our view model and the change will be reflected by the View, simple right? i advice you to check out swiftui tutorials.
struct Auth : View {
#StateObject var viewModel = ViewModel()
// #Binding var isAuth: Bool // should use binding is isAuth will be passed by a parent view
var body : some View {
if viewModel.isAuth {
Text("WelcomeView()")
} else {
VStack {
Divider()
Text("Please, wait a minute...")
Divider()
}
.frame(width: 450, height: 350)
}
}
}
class ViewModel : ObservableObject {
#Published var isAuth = false
func fetchUsers() {
db.collection("users").getDocuments { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
var newUser = UserData()
newUser.login = document["login"] as! String
newUser.name = document["name"] as! String
newUser.password = document["password"] as! String
newUser.accessLevel = document["access_level"] as! Int
self.usersList.append(newUser)
}
//
if validateUser() {
self.isAuth = true
}
//
}
}
}
}

ListView in child view is not refreshed correctly

There is a ListView. I make a transaction in Cloud Firestore by changing the field of an element when I click on it in the list. Data in the database changes as it should, but after this action all the elements in the list disappear (although there is .onAppear {fetchData}). An important point: this is a child view, there is no such problem in the parent view.
I also added a button at the bottom of the list to execute fetchData (), when I click on it, the data returns to the list
What could be the problem? Thanks
import SwiftUI
struct SecondView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
VStack {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Button("Update data"){
let updBook = book
self.viewModel.myTransaction(book: updBook)
}
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.navigationBarTitle("Books")
.onAppear() {
self.viewModel.fetchData()
}
Button("update list"){
self.viewModel.fetchData()
}
}
}
}
ViewModel:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
}
}
func deleteBook(book: Book){
if let bookID = book.id{
db.collection("books").document(bookID).delete()
}
}
func updateBook(book: Book) {
if let bookID = book.id{
do {
try db.collection("books").document(bookID).setData(from: book) }
catch {
print(error)
}
}
}
func addBook(book: Book) {
do {
let _ = try db.collection("books").addDocument(from: book)
}
catch {
print(error)
}
}
func myTransaction(book: Book){
let bookID = book.id
let targetReference = db.collection("books").document(bookID!)
db.runTransaction({ (transaction, errorPointer) -> Any? in
let targetDocument: DocumentSnapshot
do {
try targetDocument = transaction.getDocument(targetReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let oldValue = targetDocument.data()?["pages"] as? Int else {
let error = NSError(
domain: "AppErrorDomain",
code: -1,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(targetDocument)"
]
)
errorPointer?.pointee = error
return nil
}
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
transaction.updateData(["pages": oldValue + 1], forDocument: targetReference)
return nil
}) { (object, error) in
if let error = error {
print("Transaction failed: \(error)")
} else {
print("Transaction successfully committed!")
}
}
}
}
Parent view:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Button("Update"){
let delBook = book
self.viewModel.myTransaction(book: delBook)
}
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.navigationBarTitle("Books")
.onAppear() {
self.viewModel.fetchData()
}
NavigationLink(destination: SecondView()){
Text("Second View")
}
}
}
}
}
A possible solution might be that your Views and its ViewModels interfere with each other. It looks like you create two instances of the same BookViewModel:
struct ContentView: View {
#ObservedObject var viewModel = BooksViewModel()
struct SecondView: View {
#ObservedObject var viewModel = BooksViewModel()
Try creating one BooksViewModel and pass it between views (you can use an #EnvironmentObject).

Entries from text field are not adding into list

import SwiftUI
import Firebase
import FirebaseFirestore
struct ContentView: View {
var body: some View {
customView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct customView : View{
#State var msg = ""
#ObservedObject var datas = observer()
var body : some View{
VStack{
List{
ForEach(datas.data){i in
Text(i.msg)
}
.onDelete { (index) in
let id = self.datas.data[index.first!].id
let db = Firestore.firestore().collection("msgs")
db.document(id).delete{(err) in
if err != nil{
print((err!.localizedDescription))
return
}
print("deleted Successfully !!!")
self.datas.data.remove(atOffsets: index)
}
}
}
HStack{
TextField("msg", text: $msg).textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
print(self.msg)
self.addData(msg1: self.msg)
}) {
Text("Add")
}.padding()
}.padding()
}
}
func addData(msg1:String){ 'Here is the code for additon'
let db = Firestore.firestore()
let msg = db.collection("msgs").document()
msg.setData(["id":msg.documentID,"msg": msg1]) { (err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
print("Success")
self.msg = ""
}
}
}
class observer : ObservableObject{
#Published var data = [datatype]()
init() {
let db = Firestore.firestore().collection("msg")
db.addSnapshotListener{(snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
if i.type == .added{
let msgData = datatype(id: i.document.documentID, msg: i.document.get("msg")
as! String)
self.data.append(msgData)
}
}
}
}
}
struct datatype : Identifiable {
var id : String
var msg : String
}
Here is the code for the CRUD(creation, reading, update, delete) using the firebase cloud service. When i enter the text in the text field and then hit button, it adds to firebase database, but doesn't shows up in the interface in the list of the app. Can anybody tell me where i am going wrong?
In this i am trying to add data to firebase, delete it ,read it and modify/update it. But the data entered doesn't shows up in interface.
Try to update data container explicitly on main thread, like below
if i.type == .added{
let msgData = datatype(id: i.document.documentID, msg: i.document.get("msg")
as! String)
DispatchQueue.main.async {
self.data.append(msgData)
}
}
as alternate try assignment instead of modification (however this should not be the case)
DispatchQueue.main.async {
self.data = self.data + [msgData]
}