Update the data stored on Firebase by inputting new values to TextFields where there have been default values - firebase

I designed a method to show the data stored on Firebase in the textfields according to this link. Here is my solution:
import Foundation
import SwiftUI
struct TextFieldWithDefaultValue: View {
var model: userViewModel // Actual a more complex view model
var textFieldName: String
#State var editedValue: String
init(model: userViewModel, text: String) {
self.model = model
self.textFieldName = text
switch self.textFieldName {
case "Name": self._editedValue = State(wrappedValue: model.user.name)
case "Bio": self._editedValue = State(wrappedValue: model.user.bio)
case "Interest": self._editedValue = State(wrappedValue: model.user.interest)
default: self._editedValue = State(wrappedValue: "No records")
}
}
var body: some View {
TextField(textFieldName, text: $editedValue)
}
}
One of its shortages is that I cannot bind the value of the input of object State. Therefore, the value on Firebase is never changed. So my question is, is there a shortcut that allows me to update the value whenever a user's input is different from the original one? Here is the rest of my code:
#StateObject var currentUser: userViewModel
#State private var showingAlert = false
var body: some View {
TextFieldWithDefaultValue(model: currentUser, text: "Interest")
.padding()
.keyboardType(.numberPad)
.background(Color.white.opacity(0.06))
.cornerRadius(15)
Button(action: {
currentUser.updatePersonalInfo()
self.showingAlert = true
}, label: {
Text("Save")
.foregroundColor(Color("Color"))
.fontWeight(/*#START_MENU_TOKEN#*/.bold/*#END_MENU_TOKEN#*/)
.padding(.vertical)
.frame(width: UIScreen.main.bounds.width - 100)
.clipShape(Capsule())
})
.disabled(currentUser.getName() != "" || currentUser.getInterest() != "" || currentUser.getBio() != "" ? false : true)
.opacity(currentUser.getName() != "" || currentUser.getInterest() != "" || currentUser.getBio() != "" ? 1 : 0.5)
.alert(isPresented: $showingAlert) {
() -> Alert in
Alert(title: Text("Congratulations!"), message: Text("Saved successfully!"), dismissButton: .default(Text("OK")))
}
}

A workaround is to set the StateObject variable, which I pass to the TextField with the actual value in init() function in the View.
#StateObject var currentUser: userViewModel
init(currentUser: userViewModel) {
self._currentUser = StateObject(wrappedValue: currentUser)
}
TextField("Name", text: $currentUser.user.name)

You can use onChange modifier to track changes of the text and propagate it to view model like this
TextField(textFieldName, text: $editedValue)
.onChange(of: editedValue) { newValue in
viewModel.saveToFirebase(newValue)
}
The other thing you could do is make your view model anObservableObject and add editedValue there as a #Published property. Then from the View you do this:
struct TextFieldWithDefaultValue: View {
#ObservedObject var viewModel: UserViewModel
var body: some View {
TextField(textFieldName, text: viewModel.$editedValue)
}
}
and inside the view model you can use Combine to subscribe to value’s changes and save data like this:
class UserViewModel: ObservableObject {
#Published var editedValue = “”
init(…) {
// Init `editedValue` here
$editedValue
.sink { // Save data here }
.store(in: …)
}
}
The only other thing you might want to think about is that you probably don’t want to update value on Firebase every time a single character changes. But this is already out of scope of this question

Related

Swiftui Force Update View

In my content view I have a home page with some text that says "Welcome, xxxx" where xxxx is the name fetched from a firebase database. This field can be changed in the settings page that is navigated to via a Navigation Link. When the name is changed and saved the name on the home page only updates when you force shutdown the app. How do I force update the view when you press the back button from settings.
This is how I display the field:
Text("Welcome, \(companyName)")
.font(.system(size: 23))
.bold()
.foregroundColor(Color("background"))
.padding(.bottom, 50)
This is how I set a value to companyName:
func SetData() {
var db = Firestore.firestore()
let user = Auth.auth().currentUser
let userName = user?.email ?? ""
let docRef = db.collection("CONTACT").document(userName)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
self.companyName = data?["companyName"] as? String ?? ""
} else {
print("Document does not exist")
}
}
}
There are several solutions to this, but you haven't provided enough code outlining what you have done to modify the variable companyName. The easiest solution would be to pass companyName as a binding value into the settings.
What I imagine here is that your HomeView is fetching the data on launch. In the settings, a change data request is made, but nothing is done to update the data in the HomeView. By using a binding variable we can ensure that the companyName connects to the source of truth in the HomeView, and so the function modifies the companyName which is precisely the company name on the HomeView vs. modifying potentially the value of companyName.
struct HomeView: View {
#State var companyName = "Microsoft"
var body: some View {
NavigationView {
NavigationLink(destination: SettingsView(companyName: $companyName)) {
Text("Tap to navigate to Settings")
}
}
}
}
struct SettingsView: View {
#Binding var companyName : String
var body: some View {
Button {
SetData()
} label: {
HStack {
Text("Tap to change!")
Text("\(companyName)!")
}
}
}
func SetData() {
var db = Firestore.firestore()
let user = Auth.auth().currentUser
let userName = user?.email ?? ""
let docRef = db.collection("CONTACT").document(userName)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
self.companyName = data?["companyName"] as? String ?? ""
} else {
print("Document does not exist")
}
}
}
}
If you have already done this at it doesn't somehow work, another solution is to add an .onAppear modifier to your HomeView.
struct HomeView: View {
#State var companyName = "Microsoft"
var body: some View {
VStack {
// code ...
}
.onAppear {
fetchData()
}
}
func fetchData() {
// code that returns companyFetchedName
self.companyName = companyFetchedName
}
}
Modify it on main queue, like
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
DispatchQueue.main.async { // << here !!
self.companyName = data?["companyName"] as? String ?? ""
}
} else {
print("Document does not exist")
}
}

Repeated messages in chatView. how to clear view?

I have a chatView with a list of chatRow Views (messages)
each chatView has a snapshot listener with firebase, so I should get real time updates if I add a new message to the conversation
The problem I have is: when I add a new message my chatView shows ALL the messages I added before plus the new message, PLUS the same list again....if I add another message then the list repeats again
I assume I need to drop/refresh the previous views shown in the Foreach loop...how can I drop/refresh the view so it can receive refreshed NON repeated data?
struct ChatView: View {
#EnvironmentObject var chatModel: ChatsViewModel
let chat: Conversation
let user = UserService.shared.user
#State var messagesSnapshot = [Message]()
#State var newMessageInput = ""
var body: some View {
NavigationView {
VStack {
ScrollViewReader { scrollView in
ScrollView {
ForEach(chat.messages, id: \.id) { message in
if user.name == message.createdBy {
ChatRow(message: message, isMe: true)
} else {
ChatRow(message: message, isMe: false)
}
}
.onAppear(perform: {scrollView.scrollTo(chat.messages.count-1)})
}
}
Spacer()
//send a new message
ZStack {
Rectangle()
.foregroundColor(.white)
RoundedRectangle(cornerRadius: 20)
.stroke(Color("LightGrayColor"), lineWidth: 2)
.padding()
HStack {
TextField("New message...", text: $newMessageInput, onCommit: {
print("Send Message")
})
.padding(30)
Button(action: {
chatModel.sendMessageChat(newMessageInput, in: chat, chatid: chat.id ?? "")
print("Send message.")
}) {
Image(systemName: "paperplane")
.imageScale(.large)
.padding(30)
}
}
}
.frame(height: 70)
}
.navigationTitle("Chat")
}
}
}
function to add message to the conversation
func addMessagesToConv(conversation: Conversation, index: Int) {
var mensajesTotal = [Message]()
let ref = self.db.collection("conversations").document(conversation.id!).collection("messages")
.order(by: "date")
.addSnapshotListener { querySnapshotmsg, error in
if error == nil {
//loop throug the messages/docs
for msgDoc in querySnapshotmsg!.documents {
var m = Message() //emtpy struc message
m.createdBy = msgDoc["created_by"] as? String ?? ""
m.date = msgDoc["date"] as? Timestamp ?? Timestamp()
m.msg = msgDoc["msg"] as? String ?? ""
m.id = msgDoc.documentID //firebase auto id
mensajesTotal.append(m) //append this message to the total of messages
self.chats[index].messages.removeAll()
self.chats[index].messages = mensajesTotal
}
} else {
print("error: \(error!.localizedDescription)")
}
}
}
You've defined mensajesTotal outside of your snapshot listener. So, it's getting appended to every time.
To fix this, move this line:
var mensajesTotal = [Message]()
to inside the addSnapshotListener closure.
You have two options:
Clear mensajesTotal each time you get an update from the database, as #jnpdx's answer shows.
Process the more granular updates in querySnapshotmsg.documentChanges to perform increment updates in your UI, as also shown in the documentation on detecting changes between snapshots.
There is no difference in the data transferred between client and server between these approaches, so use whatever is easiest (that'd typically be #1) or most efficient on the UI (that's usually #2).

View doesn't match ObservedObject

I am using an ObservedObject with a singleton which is populated via an API call to present part of my view. However, when I update my singleton, the view isn't updated accordingly. Here is my view code:
struct Friends: View {
#State private var friends: [FirestoreUser] = []
#State private var loading = false
#ObservedObject private var currentUser = CurrentUser.shared // singleton
// Bunch of UI code ...
GeometryReader { subGeo in
VStack {
List {
ForEach(friends, id: \.id) {
UserItem(user: $0)
.background(Color.white)
}
.listRowBackground(Color.white)
}
.listRowBackground(Color.white)
Spacer()
// Some brackets and then ...
.onReceive(self.currentUser.$user, perform: { _ in
getFriends()
})
Here's the getFriends() function:
func getFriends() {
print("in get friends func")
if let friendUIDs = currentUser.user?.firestoreFriendUIDs {
print("getting friends for friends page")
if friendUIDs.count > 0 {
self.loading = true
FirestoreService.fetchUsers(uids: friendUIDs) { (friends) in
self.friends = friends!
self.loading = false
}
}
}
}
Here is my CurrentUser class:
import Foundation
import SwiftUI
class CurrentUser: ObservableObject {
#Published var user: FirestoreUser?
public static var shared = CurrentUser()
func setUser(user: FirestoreUser) {
self.user = user
}
}
And here is where I'm initially setting the singleton:
func listen() {
handle = auth.addStateDidChangeListener({ (auth, user) in
if let authUser = user {
print("user in addStateDidChangeListener is not nil")
self.loggedInFIRAuthUser = user
FirestoreService.fetchUser(uid: authUser.uid) { (user) in
DispatchQueue.main.async {
if let user = user {
CurrentUser.shared.setUser(user: user)
print("set CurrentUser singleton")
}
}
}
} else {
print("nil authUser")
}
})
}
This is part of the initial view for the app. What's happening based on print messages is that the program fetches user data from Firestore when it starts up, confirms the user has friends, but onReceived is called just before the singleton is set rather than after. I even tried adding a delay, and the singleton is actually waiting for the .OnReceived code to run. The order of the printed lines is:
in get friends func
user in addStateDidChangeListener is not nil
friendUIDs.count: 1
in get friends func
set CurrentUser singleton
Any help is greatly appreciated, I've been stuck on this for days.

Trying to show fetched firebase data on profile view SwiftUI

I'm trying to show the data I fetched from my Firebase database. I tried creating #State var variables and add them to my function but it didn't work. I tried printing my function output in a button to print it to console and it works. I just don't know how to show them in my view my code
import SwiftUI
import Firebase
struct ProfileView: View {
var body: some View {
VStack {
Button(action: {
profilef()
}) {
Text("hello")
}
HStack {
Button(action: {
try! Auth.auth().signOut()
UserDefaults.standard.set(false, forKey: "status")
NotificationCenter.default.post(name: NSNotification.Name("statusChange"), object: nil)
}) {
Text("Logout")
}
}
}
}
func profilef() {
let userID = Auth.auth().currentUser?.uid
let ref = Database.database().reference()
ref.child("UserInfo").child(userID!).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? [String : AnyObject]
let name = value?["fullName"] as? String ?? ""
print(name)
// ...
}) { error in
print(error.localizedDescription)
}
}
}
Just create a #State variable, which contains the name. If your function changes that variable, your view will updates.
struct profile: View {
#State var name : String = ""
var body: some View {
Text("Hello " + self.name)
And then in your function, instead of printing you will assign it to your state.
let name = value?["fullName"] as? String ?? ""
print(name)
self.name = name
That should work. I do not have an example with Firebase at the moment, so I can not test it. If it is not working, please describe the behavior.
Adding an #State property profileName and assigning it in the network request function will work after tapping the Button.
// ProfileView.swift
//
//
// Created by Shahin Bararesh on 2020-09-07.
//
import SwiftUI
import Firebase
struct ProfileView: View {
#State var profileName: String = ""
var body: some View {
VStack {
Button(action: {
profilef()
}) {
Text(profileName)
}
HStack {
Button(action: {
try! Auth.auth().signOut()
UserDefaults.standard.set(false, forKey: "status")
NotificationCenter.default.post(name: NSNotification.Name("statusChange"), object: nil)
}) {
Text("Logout")
}
}
}
}
func profilef() {
let userID = Auth.auth().currentUser?.uid
let ref = Database.database().reference()
ref.child("UserInfo").child(userID!).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? [String : AnyObject]
let name = value?["fullName"] as? String ?? ""
self.profileName = name
// ...
}) { error in
print(error.localizedDescription)
}
}
}

ObservedObject only passes its default value; not its assigned value. Why?

Scenario: Attempting to broadcast a variable value via an ObservableObject.
Problem: I'm only getting the default value; not the assigned value.
Here's the origin.
Button #1 starts a function to get data.
Button #2 retrieves the ObservedObject's revised value
I removed some of the vestigial code to make the presentation simpler:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
let fontCustom = Font.custom("Noteworthy", size: 23.0)
var body: some View {
ZStack {
// ...
// ...
HStack {
Button(
action: {
NetworkManager().getCalculatorIDs()
},
label: {
Text("1")
}
)
Button(
action: {
self.calculator.calculate("2");
print(self.networkManager.calculationID) // stop and check.
},
label: { Text("2") }
)
// ...
// ...
}
}
So I tap Button #1 then tap Button #2 to check if the ObservedObject has the generated id value.
I'm expecting an alphanumeric id value in the print().
Instead, I got the original value:
Royal Turkey
(lldb)
Here's the ObservableObject:
struct CalculationIdentifier: Decodable {
let id: String
let tokens: [String]
}
class NetworkManager: ObservableObject {
#Published var calculationID = "Royal Turkey"
#Published var isAlert = false
#Published var name = "Ric Lee"
let calculations = "https://calculator-frontend-challenge.herokuapp.com/Calculations"
func getCalculatorIDs() {
let urlRequest = URLRequest(url: URL(string: calculations)!)
let configuration = URLSessionConfiguration.ephemeral
let task = URLSession(configuration: configuration).dataTask(with: urlRequest) { data, _, error in
DispatchQueue.main.async {
do {
let result = try JSONDecoder().decode([CalculationIdentifier].self, from: data!)
if !result.isEmpty {
self.calculationID = (result[0] as CalculationIdentifier).id
print("Inside do{}. result = \(result)")
self.isAlert = true
} else {
print(#function, "Line:", #line, ": No Result")
}
} catch {
print(error)
}
}
}
task.resume()
}
}
BTW: Here's the local console output, the string value of 'id' should have been passed to the host as an ObservedObject value:
Inside do{}. result = [RicCalculator2.CalculationIdentifier(id: "d3dd3b1e-d9f6-4593-8c85-b8fd3d018383", tokens: [])]
So I do have a bona fide id value to send.
Why only the original value?
What am I missing?
...do I need to do a 'send' or something?
This
A. #ObservedObject var networkManager = NetworkManager()
and this
B. NetworkManager().getCalculatorIDs()
in your code are different objects, ie. you create one object as member, then other object on the stack, which does something, and then ask first object to return something - naturally if returns what it has on initialise.
Probably you assumed in case B
self.networkManager.getCalculatorIDs()

Resources