On Clicking the Button present in BillContributorView.swift, the view updates but not on Appear. I have tried swapping StateObject with ObservedObject but not only does it not help but also, I read that we should instantiate our ObservableObject with StateObject.
BillContributorView.swift
#StateObject var billManager = BillManager()
var body: some View {
VStack {
HStack {
Text("Get: \(String(format: "%.2f", billManager.toGetAmount))")
.font(.footnote)
Button {
billManager.updateToGet(contributorID: memberID)
} label: { Text("To Get") }
}
.onAppear(perform: {
billManager.updateRoomID(name: roomID)
billManager.updateToGet(contributorID: memberID )
})
}
BillManager
class BillManager: ObservableObject {
#Published var roomID: String
#Published var toGetbills = [Bill]()
#Published var toGetAmount: Double = 0
init(name: String? = nil) {
if let name = name {
roomID = name
} else {
roomID = ""
}
}
func updateRoomID(name: String) {
roomID = name
}
func updateToGet(contributorID: String) {
let collectionName = "\(roomID)_BILLS"
toGetAmount = 0
db.collection(collectionName)
.whereField("payer", isEqualTo: Auth.auth().currentUser?.uid ?? "")
.whereField("contributor", isEqualTo: contributorID)
.addSnapshotListener { snapshot, error in
DispatchQueue.main.async {
guard let doc = snapshot?.documents else {
print("No Doc Found")
return
}
self.toGetbills = doc.compactMap { queryDocumentSnapshot -> Bill? in
let result = Result { try queryDocumentSnapshot.data(as: Bill.self) }
switch result {
case .success(let bill):
return bill
}
}
}
}
toGetAmount = 0
for bill in toGetbills {
toGetAmount += bill.itemPrice
}
}
Fixed the issue by updating the value inside the closure.
db.collection(collectionName)
.whereField("payer", isEqualTo: Auth.auth().currentUser?.uid ?? "")
.whereField("contributor", isEqualTo: contributorID)
.addSnapshotListener { snapshot, error in
self.toGetAmount = 0
guard let doc = snapshot?.documents else {
print("No Doc Found")
return
}
DispatchQueue.main.async {
self.toGetbills = doc.compactMap { queryDocumentSnapshot -> Bill? in
let result = Result { try queryDocumentSnapshot.data(as: Bill.self) }
switch result {
case .success(let bill):
self.toGetAmount += bill.itemPrice
return bill
case .failure( _):
print("Failure To Get Bill Request")
return nil
}
}
}
}
Related
I'm try to fetch documents from firestore by matching the collections using the code below. I'm fetching the documents from collection("users") in case anyone update their info in the future which the new info will only uploaded to the collection("users") but not the collection("clients").
struct NewFriend: Identifiable, Hashable, Decodable {
#DocumentID var id: String?
var name: String
var uid: String
}
struct NewProspect: Identifiable, Hashable, Decodable {
#DocumentID var id: String?
var name: String
var uid: String
}
class AuthViewModel: ObservableObject {
#Published var userName = ""
#Published var friendName1 = "FriendName1"
#Published var friendName2 = "FriendName2"
#Published var friendUid1 = "friendUid1"
#Published var friendUid2 = "friendUid2"
#Published var matchingUid = ""
let friends = [NewFriend]()
let clients = [NewProspect]()
func uploadInfo() {
guard let uid = FireBaseManager.shared.auth.currentUser?.uid else { return }
let data: [String: Any] = [
"name": self.userName,
"uid": uid
]
FireBaseManager.shared.firestore.collection("users").document(uid).setData(data) { error in
if let error = error {
print("DEBUG: Failed to store user info with error: \(error.localizedDescription)")
return
}
}
}
}
func addFriend() {
guard let uid = FireBaseManager.shared.auth.currentUser?.uid else { return }
let data: [String: Any] = [
"name": self.friendName1,
"uid": self.friendUid1
]
FireBaseManager.shared.firestore.collection("users").document(uid).collections("clients").document(self.friendUid1).setData(data) { error in
if let error = error {
print("DEBUG: Failed to store user info with error: \(error.localizedDescription)")
return
}
}
func addFriend2() {
guard let uid = FireBaseManager.shared.auth.currentUser?.uid else { return }
let data: [String: Any] = [
"name": self.friendName2,
"uid": self.friendUid2
]
FireBaseManager.shared.firestore.collection("users").document(uid).collections("clients").document(self.friendUid2).setData(data) { error in
if let error = error {
print("DEBUG: Failed to store user info with error: \(error.localizedDescription)")
return
}
}
func fetchFriends() {
guard let uid = FireBaseManager.shared.auth.currentUser?.uid else { return }
FireBaseManager.shared.firestore.collection("users").document(uid).collection("clients").getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.friends = documents.compactMap({ try? $0.data(as: NewFriend.self) })
for i in 0 ..< self.friends.count {
self.matchingUid = self.friend[i].uid
print("print1: \(self.matchingUid)")
FireBaseManager.shared.firestore.collection("users")
.whereField("uid", isEqualTo: self.friendUid)
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.clients = documents.compactMap({ try? $0.data(as: NewProspect.self) })
print("print2: \(self.matchingUid)")
}
}
}
}
}
struct ListView: View {
#ObservedObject var viewModel = AuthViewModel()
var body: some View {
List {
ForEach(viewModel.clients) { client in
Text(client.name)
.padding()
}
}
.onAppear {
viewModel.fetchFriends()
}
}
}
I'm able to print both of the friends' uid in print1 but when it comes to print2 only one of the uid is printed and the list only shows one of the two friends. Any idea why is this happening? Or is there any better way to make this function work?
When you fetch users before print2, you are fetching with a where clause.
self.db.collection("users")
// This WHERE condition causes that only one friend will be found
.whereField("uid", isEqualTo: self.friendUid1)
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.clients = documents.compactMap({ try? $0.data(as: NewProspect.self) })
print("print2: \(self.matchingUid)")
}
The one in your example (self.friendUid) is not part of your AuthViewModel, I assume you meant .whereField("uid", isEqualTo: self.friendUid1)
Anyway, that where clause is limiting your results to one of the friends (in this case the friend with friendUid1 only.
Maybe you can use .whereField("uid", in: [self.friendUid1, self.friendUid2]) instead:
self.db.collection("users")
// In this case the WHERE condition cares about both friends
.whereField("uid", in: [self.friendUid1, self.friendUid2])
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.clients = documents.compactMap({ try? $0.data(as: NewProspect.self) })
print("print2: \(self.matchingUid)")
}
My view contains like-buttons, but when the view gets updated, the like-buttons get reloaded, and for a split second, the like-buttons that are liked ("heart.fill") show the neutral image for one second ("heart").
This happens when I click on other buttons that update the view.
I can't figure out why this problem occurs.
struct CommentCell: View {
#ObservedObject private var commentRowViewModel: CommentRowViewModel
init(post: Post, comment: Comment) {
commentRowViewModel = CommentRowViewModel(comment: comment, post: post)
}
var body: some View {
VStack {
Button {
commentRowViewModel.comment.didLike ?? false ?
commentRowViewModel.unlikeComment() :
commentRowViewModel.likeComment()
} label: {
let didLike = commentRowViewModel.comment.didLike ?? false
ZStack {
Image(systemName: "heart.fill")
.opacity(didLike ? 1 : 0)
.foregroundColor(Color(r: 221, g: 102, b: 91))
Image(systemName: "heart")
.opacity(didLike ? 0 : 1)
.foregroundColor(Color.black)
}
}
Text("\(commentRowViewModel.comment.likes)")
.font(.system(size: 14))
}
}
class CommentRowViewModel: ObservableObject {
#Published var comment: Comment
let service = CommentService()
let post: Post
init(comment: Comment, post: Post) {
self.comment = comment
self.post = post
checkIfUserLikedComment()
}
func likeComment() {
service.likeComment(post: post, comment: comment) { likes in
self.comment.didLike = true
self.comment.likes = likes
}
}
func checkIfUserLikedComment() {
service.checkIfUserLikedComment(comment: comment) { didLike in
if didLike {
self.comment.didLike = true
}
}
}
func unlikeComment() {
service.unlikeComment(post: post, comment: comment) {likes in
self.comment.didLike = false
self.comment.likes = likes
}
}
}
struct CommentService {
func likeComment(post: Post, comment: Comment, completion: #escaping(Int) -> Void) {
let newValue = comment.likes + 1
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {return}
guard let postId = post.id else {return}
guard let commentId = comment.id else {return}
let userLikesRef = FirebaseManager.shared.firestore.collection("users").document(uid).collection("comment-likes")
FirebaseManager.shared.firestore.collection("posts").document(postId).collection("comments").document(commentId)
.updateData(["likes": newValue]) { error in
if error != nil {
print("Failed with finding userLikes")
return
}
userLikesRef.document(commentId).setData([:]) { err in
if err != nil {
print("Failed with err", err ?? "")
return
}
completion(newValue)
}
}
}
func unlikeComment(post: Post, comment: Comment, completion: #escaping(Int) -> Void) {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {return}
guard let postId = post.id else {return}
guard let commentId = comment.id else {return}
guard comment.likes > 0 else {return}
let newValue = comment.likes - 1
let userLikesRef = FirebaseManager.shared.firestore.collection("users").document(uid).collection("comment-likes")
FirebaseManager.shared.firestore.collection("posts").document(postId).collection("comments").document(commentId)
.updateData(["likes": newValue]) { error in
if error != nil {
print("failed to unlike", error ?? "")
return
}
userLikesRef.document(commentId).delete { err in
if err != nil {
print(err ?? "")
return
}
completion(newValue)
}
}
}
func checkIfUserLikedComment(comment: Comment, completion: #escaping(Bool)->Void) {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {return}
guard let commentId = comment.id else {return}
FirebaseManager.shared.firestore.collection("users")
.document(uid)
.collection("comment-likes")
.document(commentId)
.getDocument { snapshot, error in
if error != nil {
print("Failed to fetch if user have liked post", error ?? "")
return
}
guard let snap = snapshot else {return}
completion(snap.exists)
}
}
}
I have an enum that represent my response status like this:
enum BaseModelStatus: Equatable {
case success
case fail
}
then according to my response, i will set a value to my enum like this:
#Published var baseModelStatus : BaseModelStatus? = nil
func createRoom(roomName: String, roomPassword: String) {
// add document in a collection
if !roomName.isEmpty && !roomPassword.isEmpty {
db.collection("\(roomName)").document("roomData").getDocument(source: .cache) { (document, error) in
if let document = document {
self.checkRoomName = document.get("roomName") as! String
self.baseModelStatus = BaseModelStatus.fail
} else {
print("Document does not exist in cache")
self.db.collection("\(roomName)").document("roomData").setData([
"roomName": roomName,
"roomPassword": roomPassword
]) { err in
if let err = err {
print("Error writing document: \(err)")
self.baseModelStatus = .fail
} else {
print("Document successfully written!")
self.baseModelStatus = .success
}
}
}
}
}
}
but in UI, state is not updated. According to my enum status, i will show alert but in the first try it's not showing because enum is not observed :
#ObservedObject var model = RoomViewModel()
#State private var showingAlert = false
Button(action: {
if !roomName.isEmpty && !roomPassword.isEmpty {
print("create room tapped")
model.createRoom(roomName: roomName, roomPassword: roomPassword)
if model.baseModelStatus == BaseModelStatus.success {
print("success------------------------------------------------")
}
else if model.baseModelStatus == BaseModelStatus.fail {
print("fail!!!!!!!!!!!!!!!!!!!!!!")
showingAlert = true
}
roomName = ""
roomPassword = ""
}
}
) {
Text("Create Room")
)
}
.alert("The room is already exist!", isPresented: $showingAlert) {
Button("OK", role: .cancel) { }
}
I have a small project which is an extension of a Swift UI exercise making a web call to Github from Greg Lim's book Beginning Swift UI:
https://github.com/ethamoos/GitProbe
I’ve been using this to practise basic skills and to try and add other features that could be useful in a realworld app.
My main change from the initial exercise was to add the option to choose which user to lookup (this was previously hardcoded) and allow the user to enter this. Because this can return a lot of data I would now like to make the resulting List .searchable so that the user can filter the results.
I’ve been following this tutorial here:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-search-bar-to-filter-your-data
but I’ve realised that this is based upon the data being returned being Strings, and therefore the search is a string.
I am returning JSON decoded into a list of User data objects so a straight search does not work. I am assuming that I can adjust this to match a string search against my custom objects but I'm not sure how to do this.
To give you an idea of what I mean here is the code:
import SwiftUI
import URLImage
struct Result: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [User]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct User: Codable, Hashable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
}
}
class FetchUsers: ObservableObject {
#Published var users = [User]()
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
guard let url = urlComponents.url else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let data = data {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct ContentView: View {
#State var username: String = ""
var body: some View {
NavigationView {
Form {
Section {
Text("Enter user to search for")
TextField("Enter your username", text: $username).disableAutocorrection(true)
.autocapitalization(.none)
}
NavigationLink(destination: UserView(username: username)) {
Text("Show detail for \(username)")
}
}
}
}
}
struct UserView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
#State var searchText = ""
var body: some View {
List {
ForEach(fetchUsers.users, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}.onAppear {
self.fetchUsers.search(for: username)
}
.searchable(text: $searchText)
.navigationTitle("Users")
}
/// With suggestion added
/// The search results
private var searchResults: [User] {
if searchText.isEmpty {
return fetchUsers.users // your entire list of users if no search input
} else {
return fetchUsers.search(for: searchText) // calls your search method passing your search text
}
}
}
struct UserDetailView: View {
var user: User
var body: some View {
Form {
Text(user.login).font(.headline)
Text("Git iD = \(user.id)")
URLImage(URL(string:user.avatarURL)!){ image in
image.resizable().frame(width: 50, height: 50)
}
}
}
}
Any help with this would be much appreciated.
Your UserListView is not properly constructed. I don't see why you would need a ScrollView with an empty text inside? I removed that.
So I removed searchText from the View to the FetchUsers class so we can delay the server requests thus avoiding unnecessary multiple calls. Please adjust it to your needs (check Apple's Debounce documentation. Everything should work as expected now.
import Combine
class FetchUsers: ObservableObject {
#Published var users = [User]()
#Published var searchText = ""
var subscription: Set<AnyCancellable> = []
init() {
$searchText
.debounce(for: .milliseconds(500), scheduler: RunLoop.main) // debounces the string publisher, delaying requests and avoiding unnecessary calls.
.removeDuplicates()
.map({ (string) -> String? in
if string.count < 1 {
self.users = [] // cleans the list results when empty search
return nil
}
return string
}) // prevents sending numerous requests and sends nil if the count of the characters is less than 1.
.compactMap{ $0 } // removes the nil values
.sink { (_) in
//
} receiveValue: { [self] text in
search(for: text)
}.store(in: &subscription)
}
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user.lowercased())]
guard let url = urlComponents.url else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
guard error == nil else {
print("Error: \(error!.localizedDescription)")
return
}
guard let data = data else {
print("No data received")
return
}
do {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct UserListView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
var body: some View {
NavigationView {
List {
ForEach(fetchUsers.users, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}
.searchable(text: $fetchUsers.searchText) // we move the searchText to fetchUsers
.navigationTitle("Users")
}
}
}
I hope this helps! :)
In the end, I think I've figured this out - thanks to the suggestions from Andre.
I need to correctly filter my data and then return the remainder.
Here's the corrected (abridged) version:
import SwiftUI
import URLImage
struct Result: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [User]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct User: Codable, Hashable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
}
}
class FetchUsers: ObservableObject {
#Published var users = [User]()
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
guard let url = urlComponents.url else {
return
// print("error")
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let data = data {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct ContentView: View {
#State var username: String = ""
var body: some View {
NavigationView {
Form {
Section {
Text("Enter user to search for")
TextField("Enter your username", text: $username).disableAutocorrection(true)
.autocapitalization(.none)
}
NavigationLink(destination: UserView(username: username)) {
Text("Show detail for \(username)")
}
}
}
}
}
struct UserView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
#State var searchText = ""
var body: some View {
List {
ForEach(searchResults, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}.onAppear {
self.fetchUsers.search(for: username)
}
.searchable(text: $searchText)
.navigationTitle("Users")
}
var searchResults: [User] {
if searchText.isEmpty {
print("Search is empty")
return fetchUsers.users
} else {
print("Search has a value - is filtering")
return fetchUsers.users.filter { $0.login.contains(searchText) }
}
}
}
struct UserDetailView: View {
var user: User
var body: some View {
Form {
Text(user.login).font(.headline)
Text("Git iD = \(user.id)")
URLImage(URL(string:user.avatarURL)!){ image in
image.resizable().frame(width: 50, height: 50)
}
}
}
}
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()
}
}
}