I have a Swift5 project but i decided that it would be easier to build only one of it's screen with SwiftUI.
The problem is that i don't know anything about SwiftUI so i am sorry if my question is stupid lol.
There is a TextView on the screen and i would like to load the text data from firebase.
My problem is that i can't use the loaded data in the TextView.
Can someone help me how i can use the function's variable in my VStack ( if its even possible )?
I tried putting the function in the VStack but it gave me some error.
Here is my code:
var body: some View {
List {
VStack {
Text("Just some dummy data, this is not important.")
// i want the "text" variable from loadData() to be in this Text
}
}.onAppear {
func loadData() {
guard let userdef = userdefStr(forKey: "didSelect") else { return }
self.db.collection("news").whereField("title", isEqualTo: userdef)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let text = document.data()["text"] as? String ?? ""
// this variable should be in the VStack's Text
print("text: \(text)")
Text(text)
}
}
}
}
// i think this is how i can call the loadData function,maybe i am wrong
loadData()
}
}
The answer for my question is called 'SwiftUI state variables'.
I had to make a new State variable:
#State var text = "Just some dummy data, this is not important."
In the VStack: Text(text)
Ad lastly after the let text = document.data()["text"] as? String ?? "" i put self.text = description
Related
I’ve problem with fatalError(). I try to adding to a list of words but in this line there is problem it says:
fatal Error in ContentView.swift
and i don’t why this happened my code was totally correct.
The code of this error:
// id were are * here* then there was a problem - trigger a cradh and report the error
fatalError("could not load start.txt from bundle.")
And the whole code is;
import SwiftUI
struct ContentView: View {
#State private var usedWords = [String]()
#State private var rootWord = ""
#State private var newWord = ""
var body: some View {
NavigationView {
List {
Section {
TextField("enter your word", text: $newWord)
.textInputAutocapitalization(.never)
}
Section {
ForEach(usedWords, id: \.self) { word in
HStack {
Image(systemName: "\(word.count).circle")
Text(word)
}
}
}
}
.navigationTitle(rootWord)
.onSubmit(addNewWord)
.onAppear(perform: startGame)
}
}
func addNewWord() {
// lowercase and trim the word, to make sure we don:t add duplicate words with case differences
let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
// exit if the remaining string is empty
guard answer.count > 0 else { return }
// extra validation to come
withAnimation {
usedWords.insert(answer, at: 0)
}
newWord = ""
}
func startGame() {
// 1.find the URL for start.txt in our app bundle
if let startWordsURL = Bundle.main.url(forResource: "start",withExtension: "txt") {
// 2.load start.txt into a string
if let startWords = try? String(contentsOf: startWordsURL) {
// 3.split the string up into an array of strings, splitting on line breaks
let allWords = startWords.components(separatedBy: "\n")
// 4.pcik one random word, or use "yamori" as a sensible default
rootWord = allWords.randomElement() ?? "yamori"
// if we are here everything has worked, so. we can exit
return
}
}
// id were are * here* then there was a problem - trigger a cradh and report the error
fatalError("could not load start.txt from bundle.")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any idea? How to fix that error
Thanks.
The reason for this failing is either the url for "start.txt" is nil or try? String(contentsOf: startWordsURL) fails. In order to help you debug and become an understanding why your current code design is bad consider this design:
func startGame() {
// 1.find the URL for start.txt in our app bundle
guard let startWordsURL = Bundle.main.url(forResource: "start",withExtension: "txt") else{
//assign the default value
rootWord = "yamori"
// if the url is not found this will print in the console
print("File could not be found")
return
}
do{
// 2.load start.txt into a string
let startWords = try String(contentsOf: startWordsURL)
// 3.split the string up into an array of strings, splitting on line breaks
let allWords = startWords.components(separatedBy: "\n")
// 4.pick one random word, or use "yamori" as a sensible default
rootWord = allWords.randomElement() ?? "yamori"
} catch{
//if the file couldn´t be read this will print the error in the console
print(error)
}
}
I'm trying to fetch some data from a firebase document and have it displayed in a ListView. Getting the data and decoding it into my own struct works fine (tested by letting the console print it), but it won't show up in the actual ListView. I tried to create 2 example datasets to see if there is anything wrong with the implementation of my struct and/or ListView, but those display just fine.
My best guess is that the ListView isnt happy with the id I get from the firebase document.
Here is how I fetch the data(pls ignore the ugly solution in checkAvailablePkgs() for now, that is going to change):
class firebaseController : ObservableObject {
let databaseFIRB = Firestore.firestore()
#Published var forUserAvailablePkgs : [DLPackage] = []
func checkAvailablePkgs() {
if Auth.auth().currentUser != nil {
var allAccessiblePkgs : [String] = []
let userUID = Auth.auth().currentUser!.uid
databaseFIRB.collection("user-access").document(userUID).getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
let pkgArr = dataDescription.components(separatedBy: ", ")
for partString in pkgArr {
var tmpStringSlicing : String
tmpStringSlicing = partString.replacingOccurrences(of: "\": <null>", with: "")
tmpStringSlicing = tmpStringSlicing.replacingOccurrences(of: "[\\[\\]\"]", with: "", options: .regularExpression)
allAccessiblePkgs.append(tmpStringSlicing)
}
self.checkIndividualPkgInformation(pkgIDs: allAccessiblePkgs)
} else {
print("Document does not exist")
}
}
}
}
func checkIndividualPkgInformation(pkgIDs: [String]) {
for id in pkgIDs {
databaseFIRB.collection("download-pkgs").document(id).getDocument { (document, error) in
if let document = document, document.exists {
let result = Result {
try document.data(as: DLPackage.self)
}
switch result {
case .success(let pkg):
if let pkg = pkg {
// A `Package` value was successfully initialized from the DocumentSnapshot.
print("Package: \(pkg)")
self.forUserAvailablePkgs.append(pkg)
} else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
print("Document does not exist")
}
case .failure(let error):
// A `City` value could not be initialized from the DocumentSnapshot.
print("Error decoding package: \(error)")
}
} else {
print("Package-Document does not exist")
}
}
}
}
}
This is the struct I want to save it as:
struct DLPackage: Identifiable, Codable {
#DocumentID var id = UUID().uuidString
var name : String = ""
var size : String = ""
var contentpaths : [String] = [""]
}
And here is the ListView that refuses to Display the aquired data:
struct FIRBauthView: View {
let firebaseCtrl: firebaseController = firebaseController()
var body: some View {
VStack() {
List(firebaseCtrl.forUserAvailablePkgs) {item in
HStack() {
Text(String(item.name)).font(.custom("Avenir", size: 26))
Spacer()
Text(String(item.size)).font(.custom("Avenir", size: 26))
}
}
Button {
firebaseCtrl.checkAvailablePkgs()
} label: {
Text("Check")
}
}
}
}
I can give my firebaseControler().forUserAvailablePkgs an initial value of:
forUserAvailablePkgs.append(contentsOf: [DLPackage(name: "Example", size: "1GB", contentpaths: ["hello","something"]),DLPackage(name: "Nr2", size: "1GB", contentpaths: ["hello","something else"])])
and they will show up just fine.
This is my print from the firebase data loaded into the forUserAvailablePkgs Array:
[Light_Player.DLPackage(_id: FirebaseFirestoreSwift.DocumentID<Swift.String>(value: Optional("CDB68E0C-DCE2-4FAA-AD55-872322A55E56")), name: "Example", size: "1GB", contentpaths: ["hello", "something"]),
Light_Player.DLPackage(_id: FirebaseFirestoreSwift.DocumentID<Swift.String>(value: Optional("DCAAF136-24FE-470A-B897-2D178AEDB3A0")), name: "Nr2", size: "1GB", contentpaths: ["hello", "something else"]),
Light_Player.DLPackage(_id: FirebaseFirestoreSwift.DocumentID<Swift.String>(value: Optional("package2")), name: "Bewährte Klassiker", size: "3.4GB", contentpaths: ["placeholder for another path", "placeholder for a different path"]),
Light_Player.DLPackage(_id: FirebaseFirestoreSwift.DocumentID<Swift.String>(value: Optional("package1")), name: "Basic", size: "7.3GB", contentpaths: ["placeholder for path", "placeholder for path 2"])]
I tried changing the id for the struct to:
#DocumentID var id: String?
since I found it in a tutorial online, however the code doesn't copile that way.
I also tried overwriting the ids from the firebase data with generated UUIDs after fetching the data from the database, but I still couldn't get the ListView to display it.
Any help is appreciated.
Well after a good night of sleep I was cured of my temporary blindness and found that I was missing the #ObservedObject property in my view.
struct FIRBauthView: View {
#ObservedObject var firebaseCtrl: firebaseController = firebaseController()
instead of
struct FIRBauthView: View {
let firebaseCtrl: firebaseController = firebaseController()
Now the data is showing up as intended.
I am trying to show a ProgressView when trying to export a file. The file is a large file and I am using fileExporter modifier. Having a ProgressView within a ZStack and setting the zIndex to 1 does not work. I also tried to put the long operation (getting the document) into a DispatchQueue but the challenge there is that it is not a View and does not compile. Below is the code that I currently have.
Here is what I am expecting:
After I press the "Export File" button, I should see a spinning circle while the document is being created (in this case the 2 second sleep command that simulates the long operation).
After that the "Move" sheet from fileExporter should be presented
Any help is very much appreciated.
Here is a sample application:
struct ContentView: View {
#State private var isExporting: Bool = false
var body: some View {
VStack {
Button(action: {
isExporting = true
}, label: {
Text("Export File")
})
.padding()
if isExporting {
ProgressView() {
ZStack{}
.fileExporter(isPresented: $isExporting, document: FileExport(contents: "This is a simple Text"), contentType: .plainText) { result in
if case .success = result {
print(try! result.get())
print("File Saved")
} else {
print("File Not Saved")
}
}
}
}
}
}
}
struct FileExport: FileDocument {
static var readableContentTypes: [UTType] {[.plainText]}
var contents: String
init(contents: String) {
self.contents = contents
sleep(2) // simulate a long operation
}
init(configuration: ReadConfiguration) throws {
contents = ""
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
return FileWrapper(regularFileWithContents: contents.data(using: .utf8)!)
}
}
You have a few problems in your code. First of all, your ProgressView() is not actually contained in a ZStack. The only ZStack you are showing does not contain anything, as the contents would have to be inside the curly braces. In a situation like this, you could generally just wrap your entire view in a ZStack.
Next, your own views don't generally take trailing closures like that. You didn't include your ProgressView code, but the code you have shown would be nonsensical to have as a trailing closure, so I don't think that was your intention. So, I removed the open curly brace the appropriate close curly brace.
When all of this is said and done, the view will show, but it will still be below the file export window. I have never found any way of placing a view above an OS sheet like this. I have seen some "solutions" if it is your sheet, but they were not really recommended.
Also, I am not sure how you are getting data back to show a progress view, other than just as a continuous spinner. I could not find anywhere in the docs where Apple gives you access to that data.
Lastly, because you are keying your progress view to the isPresented var isExporting means your ProgressView() will dismiss when the FileExport sheet does.
Also, while it seems to work with the .fileExporter inside the conditional if block, it feels wrong to me, and I would move it to another part of the view. It just seems like a bad practice.
FWIW, here is the "working" code, but I don't see how you can actually accomplish your goals:
struct FileExporter: View {
#State private var isExporting: Bool = false
var body: some View {
ZStack {
VStack {
Button(action: {
isExporting = true
}, label: {
Text("Export File")
})
.padding()
if isExporting {
ProgressView() // { No brace would go here
.fileExporter(isPresented: $isExporting, document: FileExport(contents: "This is a simple Text"), contentType: .plainText) { result in
if case .success = result {
print(try! result.get())
print("File Saved")
} else {
print("File Not Saved")
}
}
}
}
}
}
}
If you want to implement a solution that shows a progress view (bar or spinner), I think you would have to roll your own.
Here is a possible solution. There may be a better way to do this! I am using a ZStack{}to attach the fileExporter. Is there a different way or other solution?
struct ContentView: View {
#State private var isExporting: Bool = false
#State private var isLoading: Bool = false
#State private var document: FileExport? = nil
var body: some View {
VStack {
Button(action: {
isExporting = true
isLoading = true
DispatchQueue.global(qos: .background).async {
document = FileExport(contents: "This is a simple text")
DispatchQueue.main.async {
isLoading = false
}
}
}, label: {
Text("Export File")
})
.padding()
if isLoading {
ProgressView()
.zIndex(1.0)
}
if isExporting && !isLoading {
ZStack {}
.fileExporter(isPresented: $isExporting, document: document, contentType: .plainText) { result in
if case .success = result {
print(try! result.get())
print("File Saved")
} else {
print("File Not Saved")
}
}
}
}
}
}
I am developing a mobile app on iOS and need to track some data when a user clicks a button. However, nothing is shown when I try to get the data according to the official doc. Here is my snippet:
import SwiftUI
import FirebaseStorage
import FirebaseCore
import FirebaseFirestore
struct Station_View: View {
#State private var showingAlert = false
var ref: Firestore!
var station_ : station
var food : [food] = []
var body: some View {
VStack(alignment: .leading, spacing: 10, content: {
VStack {
ForEach(station_.menu_items, id: \.self) { i in
Divider()
.frame(width: 400, height: 1)
.background(Color("Black"))
.padding(.vertical,0)
HStack {
VStack (alignment: .leading) {
Text(i.name + ", " + i.calories + "cal, protein: " + i.protein)
.font(.headline)
.foregroundColor(Color("Black"))
}.padding(.leading, 8)
Spacer()
if (Int(i.protein)! > 10) {
Button(action: {
// print("Button action")
////////// I retrieved data here //////////////////
let docRef = ref?.collection("users").document("7lqIqxc7SGPrbRhhQWZ0rdNuKnb2")
docRef?.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
} else {
print("Document does not exist")
}
}
////////// I retrieved data here //////////////////
self.showingAlert = true
}) {
HStack {
Image(systemName: "p.circle")
Text("+50xp")
}.padding(10.0)
.overlay(
RoundedRectangle(cornerRadius: 6.0)
.stroke(lineWidth: 2.0)
)
}
.alert(isPresented: $showingAlert) {
() -> Alert in
Alert(title: Text("Congratulations!"), message: Text("You had a protein meal, XP+50!"), dismissButton: .default(Text("OK")))
}
}
if (i.is_vegan) {
Button(action: {
// print("Button action")
////////// I retrieved data here //////////////////
let docRef = ref?.collection("users").document("7lqIqxc7SGPrbRhhQWZ0rdNuKnb2")
docRef?.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
} else {
print("Document does not exist")
}
}
////////// I retrieved data here //////////////////
self.showingAlert = true
}) {
HStack {
Image(systemName: "leaf")
Text("+50xp")
}.padding(10.0)
.overlay(
RoundedRectangle(cornerRadius: 6.0)
.stroke(lineWidth: 2.0)
)
}
.alert(isPresented: $showingAlert) {
() -> Alert in
Alert(title: Text("Congratulations!"), message: Text("You had a vegan meal, XP+50!"), dismissButton: .default(Text("OK")))
}
}
}
.padding(.init(top: 12, leading: 0, bottom: 12, trailing: 0))
}
}
} )
}
}
What can I do to make it come true? I am expecting to update only one key-value pair while the others remain the same when the data is collected back.
Firstly, when working with SwiftUI you should always use a ViewModel. This is a weird transition at first but it will make your code infinitely easier to understand and keep track of. Here's the basic structure.
View Model
class YourViewModel: ObservableObject {
#Published var isTrue = false
func isValueTrue(){
print(isTrue.description)
}
}
Notice that there are a few things going on here, the ObservableObject and #Published essentially this means that the object YourViewModel can be observed with published properties, or the ones that can be bound to a view. To use it in a view you can do this.
View
struct YourView: View {
//This is your ViewModel reference.
//Use is to bind all your details to the view with
//something like yourViewModel.firstName or $yourViewModel.firstName
#observedObject var yourViewModel = YourViewModel()
var body: some View {
Button("Change Bool") {
yourViewModel.isTrue.toggle()
yourViewModel.isValueTrue()
}
}
}
This is the basic structure for an MVVM pattern and will save you tons of space in your view, making it much much easier to read and maintain. Typically you'll have a separate .swift file for the View and for the ViewModel try not to combine them, and abstract as much as you can.
To answer the ultimate question, how do you retrieve data from Firebase and update that same data? Well, the answer is as follows, I will demonstrate using a function and a property within a ViewModel that you can Bind to your views to update them.
Getting Firebase Data
//These properties are a part of the VIEWMODEL and can be bound to the view
//Using yourViewModel.someProperty
#Published var firstName = ""
#Published var lastName = ""
#Published var email = ""
func fetchFirebaseData() {
guard let uid = Auth.auth().currentUser?.uid else {
print("Handle Error")
return
}
//Create Database Reference
let db = Firestore.firestore()
//Reference the collection and document. In this example
//I'm accessing users/someUserId
let fsUserProfile = db.collection("users").document(uid)
//Request the document
fsUserProfile.getDocument { (snapshot, err) in
if err != nil { return }
self.fetchImageFromURL(url: URL(string: snapshot?.get("profile_image_url") as? String ?? "")!)
self.firstName = snapshot?.get("first_name") as? String ?? ""
self.lastName = snapshot?.get("last_name") as? String ?? ""
self.email = snapshot?.get("email") as? String ?? ""
}
}
Updating Firebase Data
This is a simple way of updating your firebase data. This is handled by passing a dictionary with a key which is the field and a value associated with it. WARNING: DO NOT use setData(...) it will clear everything else that you had in there. setData(...) is useful for first time data creation such as registering an account, creating a new entry, etc..
func updateFirebaseData(firstName: String) {
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).updateData(["first_name": firstName])
}
}
Usage
struct YourView: View {
#observedObject var yourViewModel = YourViewModel()
var body: some View {
VStack {
//Fetching Example
VStack {
Button("Fetch Data") {
yourViewModel.fetchFirebaseData()
}
Text(yourViewModel.firstName)
}
//Setting Example
VStack {
Button("Update Data") {
//You could have this "John" value, a property
//of your ViewModel as well, or a text input, or whatever
//you want.
yourViewModel.updateFirebaseData(firstName: "John")
}
}
}
}
}
Notice how much cleaner the MVVM structure is when working in SwiftUI, once you do it for a few days, it will become second nature.
I'm using Firestore with SwiftUI and I try to make a simple query to get documents from a sub-collection:
struct MyCardsView: View {
#State var myCards = [MyCard]()
#State var show: Bool = false
#State var selectedId: String = ""
var body: some View {
VStack {
List(self.myCards){i in
Button(action: {
self.selectedId = i.card_id
self.show.toggle()
}) {
MyCardCell(cardId: i.card_id)
}
}
}
.onAppear {
let db = Firestore.firestore()
if let uid = Auth.auth().currentUser?.uid {
db.collection("Users").document(uid).collection("Fiches").order(by: "date", descending: true).addSnapshotListener { (snap, err) in
guard let documents = snap?.documents else {
print("No documents")
return
}
self.myCards = documents.compactMap({ (queryDocumentSnapshot) -> MyCard? in
return try? queryDocumentSnapshot.data(as: MyCard.self)
})
}
}
}
.sheet(isPresented: self.$show){
CardsView(cardId: self.selectedId)
}
}
}
This view has always worked, but since yesterday I get the following very strange error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'FIRESTORE INTERNAL ASSERTION FAILED: Invalid document reference. Document references must have an even number of segments, but Users has 1'
I really don't understand because the path is correct and the sub-collection exists...
Im having this same issue, but only in my canvas previews. The app runs fine. I've looked through my code and have figured it to come down to this block of code, because theres only one reference to the collection and not to a document ID, however on the Firebase documentation, it says that this code is good even though it only has one segment. (although I'm just taking a guess)
public func fetchAllArtists(completion: #escaping ([Artist]) -> Void) {
var _artists: [Artist] = []
database
.collection(ContainerNames.artists)
.getDocuments { snapshot, error in\
guard error == nil else { return }
for document in snapshot!.documents {
// do stuff
completion(_artists)
}
}