SwiftUI: How to retrieve data from Firestore in second view? - google-cloud-firestore

This is my first SwiftUI project and am very new in programming.
I have a View Model and a picker for my Content View, manage to send $selection to Detail View but not sure how to get it to read from the Firestore again. I kinda feel like something is missing, like I need to use an if-else, but can't pinpoint exactly what is the problem. Searched through the forum here but can't seemed to find a solution.
Here is the VM
import Foundation
import Firebase
class FoodViewModel: ObservableObject {
#Published var datas = [Food]()
private var db = Firestore.firestore()
func fetchData(){
db.collection("meals").addSnapshotListener { (snap, err) in
DispatchQueue.main.async {
if err != nil {
print((err?.localizedDescription)!)
return
} else {
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("name") as? String ?? ""
let weight = i.document.get("weight") as? Int ?? 0
let temp = i.document.get("temp") as? Int ?? 0
let time = i.document.get("time") as? Int ?? 0
self.datas.append(Food(id: id, name: name, weight: weight, temp: temp, time: time))
}
}
}
}
}
}
struct ContentView: View {
#ObservedObject var foodDatas = FoodViewModel()
#State private var id = ""
#State public var selection: Int = 0
var body: some View {
NavigationView{
let allFood = self.foodDatas.datas
ZStack {
Color("brandBlue")
.edgesIgnoringSafeArea(.all)
VStack {
Picker(selection: $selection, label: Text("Select your food")) {
ForEach(allFood.indices, id:\.self) { index in
Text(allFood[index].name.capitalized).tag(index)
}
}
.onAppear() {
self.foodDatas.fetchData()
}
Spacer()
NavigationLink(
destination: DetailView(selection: self.$selection),
label: {
Text("Let's make this!")
.font(.system(size: 20))
.fontWeight(.bold)
.foregroundColor(Color.black)
.padding(12)
})
}
And after the picker selected a food type, I hope for it to display the rest of details such as cooking time and temperature. Now it is displaying 'index out of range' for the Text part.
import SwiftUI
import Firebase
struct DetailView: View {
#ObservedObject var foodDatas = FoodViewModel()
#Binding var selection: Int
var body: some View {
NavigationView{
let allFood = self.foodDatas.datas
ZStack {
Color("brandBlue")
.edgesIgnoringSafeArea(.all)
VStack {
Text("Cooking Time: \(allFood[selection].time) mins")
}
}
}
}
Appreciate for all the help that I can get.

In your DetailView, you're creating a new instance of FoodViewModel:
#ObservedObject var foodDatas = FoodViewModel()
That new instance has not fetched any data, and thus the index of the selection is out of bounds, because its array is empty.
You could pass your original ContentView's copy of the FoodDataModel as a parameter.
So, the previous line I quoted would become:
#ObservedObject var foodDatas : FoodViewModel
And then your NavigationLink would look like this:
NavigationLink(destination: DetailView(foodDatas: foodDatas, selection: self.$selection) //... etc

Related

Change values in struct model

I'm stuck with adding items from struct to favorites. The idea:
I have a json with data for cards
On the main screen app shows a random card
The user could pick another random card or save it to favorites.
Code below.
Creating a CardModel (file 1):
struct CardModel: Hashable, Codable, Identifiable {
let id: Int
let topic: String
let category: String
var saved: Bool
}
Retrieving data from json and creating an array of structs (file 2):
var cardsModelArray: [CardModel] = load("LetsTalkTopics.json")
Func to pickup random item from the array (file 3):
func pickRandomCard() -> CardModel {
let randomCard = cardsModelArray.randomElement()!
return randomCard
}
Func to change the "saved" bool value (file 4)
func saveCard(card: CardModel) {
let index = card.id
cardsModelArray[index] = CardModel(id: index, topic: card.topic, category: card.category, saved: !card.saved)
}
View file (file 5, simplified)
import SwiftUI
struct StackOverFlow: View {
#State var currentCard = pickRandomCard()
var body: some View {
VStack{
CardViewStackOver(cards: currentCard)
Button("Show random card") {
currentCard = pickRandomCard()
}
Button("Save item") {
saveCard(card: currentCard)
}
}
}
struct StackOverFlow_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
StackOverFlow()
}
}
}
struct CardViewStackOver: View {
let cards: CardModel
var body: some View {
VStack {
Text(cards.topic)
Text(cards.category)
Text(String(cards.id))
HStack {
if cards.saved {
Image(systemName: "heart.fill")
.font(.title)
.padding(15)
.padding(.bottom, 20)
} else {
Image(systemName: "heart")
.font(.title)
.padding(15)
.padding(.bottom, 20)
}
}
}
}
}
But I definitely making something wrong, in a separate view I'm showing saved cards but it doesn't work (it shows some random cards, and some saved cards have duplicates). With my research, I found out that structs are immutable and when I'm trying to edit a value, basically swift creates a copy of it and makes changes in the copy. If so, what would be the right approach to create this favorite feature?
A solution that fixed the problem:
func saveCardMinus(currentCard: CardModel) {
var index = currentCard.id - 1
cardsModelArray[index].saved.toggle()
}
But I'm sure that the whole solution is bad. What is the right/more proper way to solve it?
(and btw, now I face another problem: the icon for bool value updates is not in real-time, you need to open this card again to see a new value (filled heart/unfilled heart))
You could use a Binding to pass the selected card on. But I restructured the whole code as there were multiple bad practices involved:
struct StackOverFlow: View {
//store the current selected index and the collection as state objects
#State var currentCardIndex: Int?
#State var cards: [CardModel] = []
var body: some View {
VStack{
//if there is an index show the card
if let index = currentCardIndex{
CardViewStackOver(card: $cards[index])
}
Button("Show random card") {
// just create a random index
currentCardIndex = (0..<cards.count).randomElement()
}
Button("Save / delete item") {
if let index = currentCardIndex{
//you save delete as favorite here
cards[index].saved.toggle()
}
}
}.onAppear{
//donĀ“t exactly know where this function lives
if cards.count == 0{
cards = load("LetsTalkTopics.json")
}
}
}
}
struct CardViewStackOver: View {
//use binding wrapper here
#Binding var card: CardModel
var body: some View {
VStack {
Text(card.topic)
Text(card.category)
Text(String(card.id))
HStack {
Image(systemName: card.saved ? "heart.fill" : "heart")
.font(.title)
.padding(15)
.padding(.bottom, 20)
.onTapGesture {
card.saved.toggle()
}
}
}
}
}

SwifUI : pass a stored value (via user defaults) to another view

I used user defaults to save the values from a form. I wish to pass one of those values ("nationality") to another view.
I tried using the view model in order to do so, but I get an empty value in the text where I wish to pass this data.
first here is a look at the form
import SwiftUI
struct ProfilePageView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Form {
Section(header: Text("Personal Ifo")) {
TextField("User Name", text: $viewModel.userName)
}
//
Section(header: Text("Your Nationality")) {
Picker(selection: $viewModel.nationality, label: Text("You were born in")) {
ForEach(viewModel.nationalities, id: \.self) { nationality in
Text(nationality)
}
}
} //nationality section end
//
Section(header: Text("Country of residence")) {
Picker(selection: $viewModel.countryOfResidence, label: Text("Where do you live ?")) {
ForEach(viewModel.countriesOfResidence, id: \.self) { residence in
Text(residence)
}
}
} // country of residence section end
//
}
.navigationTitle("Profile")
//form end
}
}
second here is my view model, which enables me to save the form values to user default
import Foundation
class ViewModel: ObservableObject {
#Published var userName: String {
didSet {
UserDefaults.standard.setValue(userName, forKey: "username")
}
}
//nationality
#Published var nationality: String {
didSet {
UserDefaults.standard.setValue(nationality, forKey: "nationality")
}
}
public var nationalities = ["USA", "Japan", "Senegal", "France"]
//country of residence
#Published var countryOfResidence: String {
didSet {
UserDefaults.standard.setValue(countryOfResidence, forKey: "country of residence")
}
}
public var countriesOfResidence = ["USA", "Japan", "Senegal", "France"]
init() {
self.userName = UserDefaults.standard.object(forKey: "username") as? String ?? ""
self.nationality = UserDefaults.standard.object(forKey: "nationality") as? String ?? ""
self.countryOfResidence = UserDefaults.standard.object(forKey: "Coutry of residence") as? String ?? ""
}
}
finally here is the view where I wish to attach this value. I am using #observedObject in order to try an retrieve the "nationality" value from my view model. however the value is not passed in the text
import SwiftUI
struct VisaPolicyDetailView: View {
#Binding var country: Country
#ObservedObject var viewmodel = ViewModel()
var body: some View {
VStack(alignment: .leading,spacing: 20) {
//a dismiss button
HStack {
Spacer()
Image(systemName: "xmark")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
}
Spacer()
Text(country.countryName)
.font(.title)
.fontWeight(.medium)
Text("Visa policy for \(viewmodel.nationality) : visa policy")
.font(.title2)
Spacer()
}
.padding()
.navigationTitle("Visa Policy Detail")
}
}
In the provided scenario it is better to use shared ViewModel, like
class ViewModel: ObservableObject {
static let shared = ViewModel()
// .. other code
}
and use that shared in all affected views
struct ProfilePageView: View {
#ObservedObject var viewModel = ViewModel.shared // << here !!
...
and
struct VisaPolicyDetailView: View {
#Binding var country: Country
#ObservedObject var viewmodel = ViewModel.shared // << here !!
...
then both views will observe same instance of ViewModel.
SwiftUI 2.0: use AppStorage to automatically store/observe user defaults, like
#AppStorage("nationality") var nationality = "some default here"

How to load TextEditor with previously saved coreData

I am trying to build a simple notes style app using SwiftUI. I have managed to create an entity (ApplicationQuestion), use a TextEditor to save user input (question.answer) to an attribute of this entity and display this attribute to a view.
However, rather than save and overwrite the question.answer each time I would like to be able to edit it.
I can only find examples doing this with UIkit and the documentation doesn't explain how you can load text into the editor. I can't work out if this isn't achievable with SwiftUI and I need to use a different approach.
The answer composer currently looks like this:
struct answerComposer: View {
let question: ApplicationQuestion
#Environment(\.managedObjectContext) var moc
#State var answerText: String = ""
#State private var currentWordCount: Int = 0
var body: some View {
VStack{
//Title
Text(self.question.title?.uppercased() ?? "Unknown Title")
.font(.largeTitle)
//Detail
Text(self.question.detail ?? "Unknown detail")
.font(.footnote)
.fontWeight(.light)
//answer
Text(self.question.answer ?? "Unknown answer")
.font(.body)
.padding()
//text editor
TextEditor(text: $answerText)
.allowsTightening(true)
.onChange(of: answerText) { value in
let words = answerText.split { $0 == " " || $0.isNewline }
self.currentWordCount = words.count
}
.padding()
// Save Button
Button("Save") {
// let question = ApplicationQuestion(context: self.moc)
question.answer = self.answerText
try? self.moc.save()
}
}
}
}
I assume you wanted this
struct answerComposer: View {
let question: ApplicationQuestion
#Environment(\.managedObjectContext) var moc
#State private var currentWordCount: Int = 0
#State private var answerText: String
init(question: ApplicationQuestion) {
self.question = question
self._answerText = State(initialValue: question.answer ?? "") // << here !!
}
// ... other code
}

SwiftUI SceneDelegate - contentView Missing argument for parameter 'index' in call

I am trying to create a list using ForEach and NavigationLink of an array of data.
I believe my code (see the end of the post) is correct but my build fails due to
"Missing argument for parameter 'index' in call" and takes me to SceneDelegate.swift a place I haven't had to venture before.
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
I can get the code to run if I amend to;
let contentView = ContentView(habits: HabitsList(), index: 1)
but then all my links hold the same data, which makes sense since I am naming the index position.
I have tried, index: self.index (which is what I am using in my NavigationLink) and get a different error message - Cannot convert value of type '(Any) -> Int' to expected argument type 'Int'
Below are snippets of my code for reference;
struct HabitItem: Identifiable, Codable {
let id = UUID()
let name: String
let description: String
let amount: Int
}
class HabitsList: ObservableObject {
#Published var items = [HabitItem]()
}
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var index: Int
var body: some View {
NavigationView {
List {
ForEach(habits.items) { item in
NavigationLink(destination: HabitDetail(habits: self.habits, index: self.index)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}
}
}
}
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var habits: HabitsList
var index: Int
var body: some View {
NavigationView {
Form {
Text(self.habits.items[index].name)
}
}
}
}
You probably don't need to pass the whole ObservedObject to the HabitDetail.
Passing just a HabitItem should be enough:
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
let item: HabitItem
var body: some View {
// remove `NavigationView` form the detail view
Form {
Text(item.name)
}
}
}
Then you can modify your ContentView:
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var body: some View {
NavigationView {
List {
// for every item in habits create a `linkView`
ForEach(habits.items, id:\.id) { item in
self.linkView(item: item)
}
}
}
}
// extract to another function for clarity
func linkView(item: HabitItem) -> some View {
// pass just a `HabitItem` to the `HabitDetail`
NavigationLink(destination: HabitDetail(item: item)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}

SwiftUI: How to Save value from a TextField in core data

I am building a very simple SwiftUI App, following a tutorial.
I created a View that contains a list of items which is #fetched from CoreData, and the list is shown with no problem.
Then I added a modal window with a TextField and a Button to save the data.
This is the code of the modal:
import SwiftUI
struct AddCategoryView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#Environment(\.managedObjectContext) var context
#State public var name: String = ""
#State public var id = UUID()
var body: some View {
VStack {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Close")
}
Text("Add a new Category")
.font(.largeTitle)
TextField("Enter category name", text: $name)
.padding(.all)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
let category = Category(context: self.context)
category.name = self.name
category.id = self.id
do {
try self.context.save()
} catch {
self.name = "There is an error!"
}
}) {
Text("SAVE \(self.name)")
}
}
}
}
struct AddCategoryView_Previews: PreviewProvider {
static var previews: some View {
AddCategoryView()
}
}
In this line,
Text("SAVE \(self.name)")
I am printing (for debugging) the value of the variable name, and I can see that the variables changes to the value that is in the catch statement
self.name = "There is an error!"
So I know that the saving is failing. But I have no idea why.
In the List View I have a button that will open this modal; I changed the value of this Button from
self.showAddModal.toggle()
TO
let category = Category(context: self.context)
category.name = "New Category"
do {
try self.context.save()
} catch {
self.name = "There is an error!"
}
And it works! I can see that the List View is updated with the value.
This is the code for the List View
struct CategoriesView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: Category.entity(), sortDescriptors: []) var categories: FetchedResults<Category>
#State private var showAddModal = false;
#State public var name = ""
var body: some View {
VStack {
Text("\(name)")
List {
ForEach(categories, id: \.id) { category in
VStack {
Text(category.name ?? "Unknown")
}
}
}
Button(action: {
let category = Category(context: self.context)
category.name = "New Category"
do {
try self.context.save()
} catch {
self.name = "There is an error!"
}
}) {
Text("+")
.font(.system(.largeTitle))
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.frame(width: 48, height: 48)
.background(Color.purple)
.cornerRadius(24)
.padding(.all)
.shadow(color: Color.black.opacity(0.3),
radius: 3,
x: 3,
y: 3)
}.sheet(isPresented: $showAddModal) {
AddCategoryView()
}
}
}
}
struct CategoriesView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return CategoriesView().environment(\.managedObjectContext, context)
}
}
I have spent the last 2 hours looking and googling for a solution, but I cannot find out what is wrong in my code. I also tried on the simulator but got the same error; I cannot save the core data from the TextField.
Thanks in advance!
Ok I finally found the solution, which maybe is not the right one but at least it works.
.sheet(isPresented: $showAddModal) {
AddCategoryView().environment(\.managedObjectContext, self.managedObjectContext)
}
Yep, just passing the Environment with the context to the Modal View fixed the issue. I am not sure this is the correct way to do it.