SwiftUI Read Data from Firebase to View and Function - swift

I'm still a beginner and have a question, I'm trying to read data from the database with Firebase how can I pass this to a text? I want to pass "testUser" and display it.
anyone have an idea :-)
import SwiftUI
import FirebaseDatabase
import Firebase
struct uebergeben: View {
var body: some View {
VStack {
Text(datenLesen)
}
}
func datenLesen() {
let user = Auth.auth().currentUser!.uid
Database.database().reference().child("benutzer").child(user).child("nuterinfo").child("nutzername").observeSingleEvent(of: .value, with: { (snapshot) in
if let testUser = snapshot.value as? String {
print(testUser)
}
})
}
}
struct uebergeben_Previews: PreviewProvider {
static var previews: some View {
uebergeben(test: "Daten Abrufen")
}
}

You can use the #State properties inside of your view. For more details about #State you can read in the documentation. In short, the #State property is a property that is responsible for the state of the view. When the value of this property changes it means that the state of the views also changes and our view redraws itself.
struct uebergeben: View {
#State
private var user: String = "" // Declare the view's state property
var body: some View {
VStack {
Text(user)
}
.onAppear {
datenLesen() // Call the method for the fetching data
}
}
func datenLesen() {
let user = Auth.auth().currentUser!.uid
Database.database().reference().child("benutzer").child(user).child("nuterinfo").child("nutzername").observeSingleEvent(of: .value, with: { (snapshot) in
if let testUser = snapshot.value as? String {
self.user = user // Change the value of the state property of the view
}
})
}
}
Feel free if want to ask something additional about this or you have some questions for code

Related

Firestore method in view model not invoking in SwiftUI onAppear method

I want to implement a Text field that displays the current user's existing score in the DB (Firestore). Because of the nature of async in Firebase query, I also need to do some adjustment in my codes. However, it seems that completion() handler does not work well:
// ViewModel.swift
import Foundation
import Firebase
import FirebaseFirestore
class UserViewModel: ObservableObject {
let current_user_id = Auth.auth().currentUser!.uid
private var db = Firestore.firestore()
#Published var xp:Int?
func fetchData(completion: #escaping () -> Void) {
let docRef = db.collection("users").document(current_user_id)
docRef.getDocument { snapshot, error in
print(error ?? "No error.")
self.xp = 0
guard let snapshot = snapshot else {
completion()
return
}
self.xp = (snapshot.data()!["xp"] as! Int)
completion()
}
}
}
// View.swift
import SwiftUI
import CoreData
import Firebase
{
#ObservedObject private var users = UserViewModel()
var body: some View {
VStack {
HStack {
// ...
Text("xp: \(users.xp ?? 0)")
// Text("xp: 1500")
.fontWeight(.bold)
.padding(.horizontal)
.foregroundColor(Color.white)
.background(Color("Black"))
.clipShape(CustomCorner(corners: [.bottomLeft, .bottomRight, .topRight, .topLeft], size: 3))
.padding(.trailing)
}
.padding(.top)
.onAppear() {
self.users.fetchData()
}
// ...
}
}
My result kept showing 0 in Text("xp: \(users.xp ?? 0)"), which represents that the step is yet to be async'ed. So what can I do to resolve it?
I would first check to make sure the data is valid in the Firestore console before debugging further. That said, you can do away with the completion handler if you're using observable objects and you should unwrap the data safely. Errors can always happen over network calls so always safely unwrap anything that comes across them. Also, make use of the idiomatic get() method in the Firestore API, it makes code easier to read.
That also said, the problem is your call to fetch data manually in the horizontal stack's onAppear method. This pattern can produce unsavory results in SwiftUI, so simply remove the call to manually fetch data in the view and perform it automatically in the view model's initializer.
class UserViewModel: ObservableObject {
#Published var xp: Int?
init() {
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let docRef = Firestore.firestore().collection("users").document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let xp = doc.get("xp") as? Int {
self.xp = xp
} else if let error = error {
print(error)
}
}
}
}
struct ContentView: View {
#ObservedObject var users = UserViewModel()
var body: some View {
VStack {
HStack {
Text("xp: \(users.xp ?? 0)")
}
}
}
}
SwiftUI View - viewDidLoad()? is the problem you ultimately want to solve.

SwiftUI - Show the data fetched from Firebase in view?

Firebase I am trying to show data taken from Firestore in my SwiftUI view but I have a problem. I have no problem with pulling data from Firebase. But I cannot show the data as I want. I work with MVVM architecture.
My model is like this:
struct UserProfileModel: Identifiable {
#DocumentID var id : String?
var username : String
var uidFromFirebase : String
var firstName : String
var lastName : String
var email : String
}
ViewModel:
class UserProfileViewModel: ObservableObject {
#Published var user : [UserProfileModel] = []
private var db = Firestore.firestore()
func data(){
db.collection("Users").whereField("uuidFromFirebase", isEqualTo: Auth.auth().currentUser!.uid).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No Documents")
return
}
self.user = documents.compactMap { queryDocumentSnapshot -> UserProfileModel? in
return try? queryDocumentSnapshot.data(as: UserProfileModel.self)
}
}
}
}
View:
struct MainView: View {
#ObservedObject private var viewModel = UserProfileViewModel()
var body: some View { // -> Error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
VStack{
Text(viewModel.user.username) // -> I want to do this but XCode is giving an error.
// This works but I don't want to do it this way.
List(viewModel.user) { user in
VStack{
Text(user.username)
Text(user.firstName)
Text(user.lastName)
Text(user.email)
Text(user.uidFromFirebase)
}
}
}
}
}
In the videos and articles titled "SwiftUI fetch data from Firebase" that I watched and read, I have always narrated on List and ForEach. But I want to use the data wherever. I shared all my code with you. I want to learn how I can do this.
Looks to me like you really just want to have one user that you're pulling the data for, but you've set up your UserProfileViewModel with an array of users ([UserProfileModel]). There are a number of ways that you could take care of this. Here's one (check the code for inline comments about what is going on):
class UserProfileViewModel: ObservableObject {
#Published var user : UserProfileModel? = nil // now an optional
private var db = Firestore.firestore()
func data(){
db.collection("Users").whereField("uuidFromFirebase", isEqualTo: Auth.auth().currentUser!.uid).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No Documents")
return
}
self.user = documents.compactMap { queryDocumentSnapshot -> UserProfileModel? in
return try? queryDocumentSnapshot.data(as: UserProfileModel.self)
}.first // Take the first document (since there probably should be only one anyway)
}
}
}
struct MainView: View {
#ObservedObject private var viewModel = UserProfileViewModel()
var body: some View {
VStack {
if let user = viewModel.user { //only display data if the user isn't nil
Text(user.username)
Text(user.firstName)
Text(user.lastName)
Text(user.email)
Text(user.uidFromFirebase)
}
}
}
}
I'd say a more traditional way of handling this might be to store your user profile document Users/uid/ -- that way you can just user document(uid) to find it rather than the whereField query.

Storing a selected value from picker to use in other views - SwiftUI

I was very kindly helped to allow my picker to select a value from my Firestore database. What I would like to do is once that value is selected in my picker I would like to be able to show that value in other views. I have tried setting this up using UserDefaults but I'm not sure that's the way to go? If you could suggest a better method I'd be more than grateful. My code currently is below.
The value in the below code returns Unknown School each time but without the user defaults works flawlessly in fetching the data.
Thanks in advance.
import SwiftUI
import Firebase
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData()
#State private var selectedSchool = UserDefaults.standard.string(forKey: "") // `schoolName.id` is of type String
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(schoolData.datas, id: \.id) {
Text($0.name)
}
}
Text("Selected School: \(selectedSchool ?? "Unknown School")")
}
}.navigationBarTitle("School Selection")
}
}
struct SchoolPicker_Previews: PreviewProvider {
static var previews: some View {
SchoolDetailsView()
}
}
class getSchoolData : ObservableObject{
#Published var datas = [schoolName]()
init() {
let db = Firestore.firestore()
db.collection("School Name").addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("Name") as! String
self.datas.append(schoolName(id: id, name: name))
}
}
}
}
struct schoolName : Identifiable {
var id : String
var name : String
}
}
First, the UserDefaults key for your variable can't be empty:
#State private var selectedSchool: String = UserDefaults.standard.string(forKey: "selectedSchool") ?? "Unknown School"
Then you can use onReceive to update the variable:
.onReceive(Just(selectedSchool)) {
UserDefaults.standard.set($0, forKey: "selectedSchool")
}
Full code:
import Combine
import Firebase
import SwiftUI
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData()
#State private var selectedSchool = UserDefaults.standard.string(forKey: "selectedSchool")
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(schoolData.datas, id: \.id) {
Text($0.name)
}
}
.onReceive(Just(selectedSchool)) {
UserDefaults.standard.set($0, forKey: "selectedSchool")
}
Text("Selected School: \(selectedSchool)")
}
}.navigationBarTitle("School Selection")
}
}
}
Note that in SwiftUI 2 / iOS 14 you can use #AppStorage instead.

SwiftUI published variable not updating

I'm trying to set a published variable named allCountries. In my DispatchQueue, it's being set and is actually printing out the correct information to the console. In my content view, nothing is in the array. In the init function on the content view, I'm calling the the function that will fetch the data and set the variable but when I print to the console, the variable is blank. Can someone tell me what I'm doing wrong?
Here is where I'm fetching the data
class GetCountries: ObservableObject {
#Published var allCountries = [Countries]()
func getAllPosts() {
guard let url = URL(string: "apiURL")
else {
fatalError("URL does not work!")
}
URLSession.shared.dataTask(with: url) { data, _, _ in
do {
if let data = data {
let posts = try JSONDecoder().decode([Countries].self, from: data)
DispatchQueue.main.async {
self.allCountries = posts
print(self.allCountries)
}
} else {
DispatchQueue.main.async {
print("didn't work")
}
}
}
catch {
DispatchQueue.main.async {
print(error.localizedDescription)
}
}
}.resume()
}
}
Here's my content view
struct ContentView: View {
var countries = ["Select Your Country", "Canada", "Mexico", "United States"]
#ObservedObject var countries2 = GetCountries()
#State private var selectedCountries = 0
init() {
GetCountries().getAllPosts()
print(countries2.allCountries)
}
var body: some View {
NavigationView {
ZStack {
VStack {
Text("\(countries2.allCountries.count)")}}}
The problem is happening in the init of your ContentView. You're calling the TYPE's function getAllPosts, but then you're trying to access the data of a new INSTANCE of that Type countries2. Your countries2.allCountries instance property really has no relationship to the GetCountries.allCountries type property.
Does that even compile? Usually you need to define a class's functions and properties as static if you want to call them on the Type itself and not on an instance of it.
Maybe it's weirdness because you're trying to do this in the ContentView's init, rather than after it has initialized.
There's probably two things you need to do to fix this.
Step one would be to let your custom initializer actually initialize countries2.
And step two would be to call countries2.getAllPosts within that custom initializer, which will populate the instance property you're trying to access in countries2.allCountries.
Try this:
struct ContentView: View {
var countries = ["Select Your Country", "Canada", "Mexico", "United States"]
// You'll initialize this in the custom init
#ObservedObject var countries2: GetCountries
#State private var selectedCountries = 0
init() {
countries2 = GetCountries()
countries2.getAllPosts()
print(countries2.allCountries)
}
var body: some View {
NavigationView {
ZStack {
VStack {
Text("\(countries2.allCountries.count)")}}}
But I'm not sure if you're going to run into problems with the async call within the ContentView initializer. It might be better practice to move that into a function which is called when the view is loaded, rather than when the SwiftUI struct is initialized.
To do that you move your networking calls into a function of ContentView (instead of its init), then use an .onAppear modifier to your view, which will call a function when the NavigationView appears. (Now that we're not using a custom initializer, we'll need to go back to initializing the countries2 property.)
struct ContentView: View {
var countries = ["Select Your Country", "Canada", "Mexico", "United States"]
// We're initializing countries2 again
#ObservedObject var countries2 = GetCountries()
#State private var selectedCountries = 0
// This is the function that will get your country data
func getTheCountries() {
countries2.getAllPosts()
print(countries2.allCountries)
}
var body: some View {
NavigationView {
ZStack {
VStack {
Text("\(countries2.allCountries.count)")
}
}
}
.onAppear(perform: getTheCountries) // Execute this when the NavigationView appears
}
}
Note with this approach. Depending on your UI architecture, every time NavigationView appears, it will call getTheCountries. If you make NavigationLink calls to new views, then it may get reloaded loaded if you go back to this first page. But you can easily add a check within getTheCountries to see if the work is actually required.
It should be used one same instance to initiate request & read results, so here is fixed variant:
struct ContentView: View {
var countries = ["Select Your Country", "Canada", "Mexico", "United States"]
#ObservedObject var countries2 = GetCountries() // member
#State private var selectedCountries = 0
init() {
// GetCountries().getAllPosts() // [x] local variable
// print(countries2.allCountries)
}
var body: some View {
NavigationView {
ZStack {
VStack {
Text("\(countries2.allCountries.count)")
}
}
}
.onAppear { self.countries2.getAllPosts() } // << here !!
}
}

Display query values within view's body

What I want to do: I want to display the user's name on the Profile view after a user logs into the app. Currently I have implemented a login using Firebase Auth. When users create an account, it creates a record in the "Users" collection in Firestore that records First and Last Name and email address.
What I've Tried: On the Profile view, I currently have a function "getUser" that checks if the user is logged in, and then matches the user's Firebase Auth email address and matches it to the record in Firestore. This is working because I've checked what info the query is returning by what's logged in the console. However, I'm at a lost how to get the "fName" information and displaying it within the view's body.
Here's screenshots of the database structure and my current code.
import SwiftUI
import Firebase
import Combine
import FirebaseFirestore
struct ProfileView: View {
#EnvironmentObject var session: SessionStore
let db = Firestore.firestore()
func getUser() {
session.listen()
let query = db.collection("users").whereField("email", isEqualTo: session.session!.email!)
query.getDocuments { (QuerySnapshot, err) in
if let docs = QuerySnapshot?.documents {
for docSnapshot in docs {
print (docSnapshot.data())
}
}
}
}
var body: some View {
Group {
if (session.session != nil) {
NavigationView {
VStack {
Text("Welcome back \(session.session!.email ?? "user")")
Spacer()
Button(action: session.signOut) {
Text("Sign Out")
}.padding(.bottom, 60)
}
} // end NavigationView
} else {
AuthView()
}
}.onAppear(perform: getUser)
}
}
I guess you should save the values returned from Firestore with UserDefaults
import UIKit
//your get user data logic here
let fName:String = <the first name retrieved from firestore>
// Get the standard UserDefaults as "defaults"
let defaults = UserDefaults.standard
// Save the String to the standard UserDefaults under the key, "first_name"
defaults.set(fName, forKey: "first_name")
And in your profile page you should retrieve this value
// you should have defaults initialized here
fnameToDisplay = defaults.string(forKey: "first_name")
Hello I prefer to make a ViewModel - NetworkManager class that comfort ObservableObject and inside of it you can get and fetch data and when they finish loading they will update the view, Let me show you an example how it works:
as you can see I created a NetworkManager extends ObservableObject (cause I want to get notified when the user finish loading) inside of it you can see that var user is annotated with #Published that make the user observed and when a new data set will notify the View to update. also you can see that I load the data on init so the fist time this class initialized it will call the fetchData() func
class NetworkManager: ObservableObject {
let url: String = "https://jsonplaceholder.typicode.com/todos/1"
var objectWillChange = PassthroughSubject<NetworkManager, Never>()
init() {
fetchData()
}
#Published var user: User? {
didSet {
objectWillChange.send(self)
print("set user")
}
}
func fetchData() {
guard let url = URL(string: url) else {return}
print("fetch data")
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {return}
print("no error")
guard let data = data else {return}
print("data is valid")
let user = try! JSONDecoder().decode(User.self, from: data)
DispatchQueue.main.async {
self.user = user
}
}.resume()
}
}
ContentView is where I'm gonna place the user data or info I declared #ObservedObject var networkManager cause it extend ObservableObject and I place the views and pass User
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
VStack {
DetailsView(user: networkManager.user)
}
}
}
struct DetailsView: View {
var user: User?
var body: some View {
VStack {
Text("id: \(user?.id ?? 0)")
Text("UserID: \(user?.userId ?? 0 )")
Text("title: \(user?.title ?? "Empty")")
}
}
}
class User: Decodable, ObservableObject {
var userId: Int = 0
var id: Int = 0
var title: String = ""
var completed: Bool = false
}
PS: I didn't make objects unwrapped correctly please consider that so
you not to have any nil exception