How to load TextEditor with previously saved coreData - swift

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
}

Related

Passing data to .popover in swiftui

I have a String value that changes after pressing the button, and I have a .popover to show it's value, but the value is not changing. I've added print() on each step to clearly show that the value had changed
My ContentView.swift
import SwiftUI
struct ContentView: View {
#State private var showingPopover = false
#State private var popoverText = "hey, I'm the initial text"
var body: some View {
Button("press me!") {
print("1) \(popoverText)")
popoverText = "hey, I'm the new text"
print("2) \(popoverText)")
showingPopover = true
print("3) \(popoverText)")
}
.popover(isPresented: $showingPopover) {
Text(popoverText)
.onAppear(perform: {print("4) \(popoverText)")})
}
}
}
After pressing the button it prints out this:
1) hey, I'm the initial text
2) hey, I'm the new text
3) hey, I'm the new text
4) hey, I'm the new text
And shows this:
Though I've found a weird workaround. Everything works fine if I add Text(popoverText):
import SwiftUI
struct ContentView: View {
#State private var showingPopover = false
#State private var popoverText = "hey, I'm the initial text"
var body: some View {
VStack{
Text(popoverText)
Button("press me!") {
print("1) \(popoverText)")
popoverText = "hey, I'm the new text"
print("2) \(popoverText)")
showingPopover = true
print("3) \(popoverText)")
}
}
.popover(isPresented: $showingPopover) {
Text(popoverText)
.onAppear(perform: {print("4) \(popoverText)")})
}
}
}
It prints the same thing:
1) hey, I'm the initial text
2) hey, I'm the new text
3) hey, I'm the new text
4) hey, I'm the new text
But now it shows the new text:
I think it has something to do with view refreshing or focus but I had no success in nailing down the exact thing
EDIT: my actual application is not with the button, code below is just made as an example so keeping .isPresented is necessary, and it's only allowed to use either .isPresented or .item
.popover(item: someIdentifiableObj, content: ...) is use to pass data into popover
struct HomeView: View {
#State var userObj: User?
var body: some View {
Button("press me!") {
userObj = User(popoverText: "New Text")
}
.popover(item: $userObj) { user in
Text(user.popoverText)
}
}
}
// MARK: - PREVIEW
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
struct User: Identifiable {
let id: UUID = UUID()
let popoverText: String
}

Update search results from api when value changes

I have a search bar that will perform a search function based off user input. When the user searches a list is populated (ie. this is a meal app, so if they search for "eggs" a list will be populated showing results of search)
My issue is when the user completes there first search and wants to type again to find a new value (food), the list does not populate again. The API still makes the call, but I'm having trouble updating the list. I tried adding removeAll() to the array onSubmit but it didn't work as expected.
struct FoodSearchResultsView: View {
//calls API
#EnvironmentObject private var foodApi: FoodApiSearch
//textfield input
#State private var searchResultsItem = ""
//if toggled, will display, binded to search bar
#Binding var userSearch: Bool
//var holds if textfield typing is complete by user
#Binding var textComplete: Bool
//triggers select breakfast, lunch, dinner optins
//when false, api results will not display
#State private var isViewSearching = false
var body: some View {
if userSearch{
VStack{
Text(isViewSearching ? "Results" : "Searching..")
Spacer()
// delays showing api call
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
self.isViewSearching = true
}
}
//if user has completed searching for a food
if isViewSearching{
List(foodApi.userSearchResults){meal in
VStack{
HStack{
VStack(alignment: .leading){
Text(meal.mealName)
HStack{
Text(meal.calories + " cals, ")
.font(.caption)
.offset(y:8)
Text(meal.brand)
.font(.caption)
.offset(y:8)
}
}
.foregroundColor(.black)
Spacer()
Image(systemName: "plus.app")
.font(.title2)
.foregroundColor(.blue)
.offset(x: 30)
}
.frame(width:200, height:40) //width of background
.padding([.leading, .trailing], 60)
.padding([.top, .bottom], 10)
.background(RoundedRectangle(
cornerRadius:20).fill(Color("LightWhite")))
.foregroundColor(.black)
Spacer()
}
}
.frame(height:800)
}
}
}
}
}
SearchBar
struct MealSearchBar: View {
//TEXTFIELD
#State var userFoodInput = ""
#State private var didtextComplete = false
//if user seached for a meal
#State private var didUserSearch = false
//calls search API
#StateObject private var foodApi = FoodApiSearch()
var body: some View {
VStack{
ZStack{
Rectangle()
.foregroundColor(Color("LightWhite"))
HStack{
Image(systemName: "magnifyingglass")
TextField("Enter Food", text: $userFoodInput)
.onSubmit {
foodApi.searchFood(userItem: userFoodInput)
didUserSearch = true
userFoodInput = ""
}
//Text(foodApi.foodDescription)
}
.foregroundColor(.black)
.padding(.leading, 13)
}
.frame(height:40)
.cornerRadius(15)
.padding(12)
}
FoodSearchResultsView(userSearch: $didUserSearch, textComplete: $didtextComplete)
.environmentObject(foodApi)
}
}
I only attached my results view and searchbar that calls the view. I believe the issue is happening onSubmit of the textfield, if you need the api call as well, will be happy to supply it, but to confirm again for clarity, despite the list not refreshing, the API is still updating, despite the list not updating.
Update: Added API Call
class FoodApiSearch: ObservableObject{
var userSearchResults: [Meal] = Array()
#Published var foodUnit = ""
#Published var calories = ""
#Published var brand = ""
//will search for user Input
func searchFood(userItem: String){
///IMPROVE API FUNCTION LATER ON DURING LAUNCH
///
let urlEncoded = userItem.addingPercentEncoding(withAllowedCharacters: .alphanumerics)
guard
let url = URL(string: "https://api.nal.usda.gov/fdc/v1/foods/search?&api_key=****GWtDvDZVOy8cqG&query=\(urlEncoded!)") else {return}
URLSession.shared.dataTask(with: url) { (data, _,_) in
let searchResults = try! JSONDecoder().decode(APISearchResults.self, from: data!)
DispatchQueue.main.async { [self] in
var counter = 0
for item in searchResults.foods ?? []{
if (counter < 5){
self.userSearchResults.append(Meal(
id: UUID(),
brand: item.brandOwner?.lowercased().firstCapitalized ?? "Brand Unavailable",
mealName: item.lowercaseDescription?.firstCapitalized ?? "food invalid",
calories: String(Double(round(item.foodNutrients?[3].value! ?? 0.00)).removeZerosFromEnd()),
quantity: 2,
amount: "test",
protein: 2,
carbs: 2,
fat: 2)
)
counter += 1
}
else{return}
}
}
}
.resume()
}
}
Thank you everyone for the advice and tips. So the actual issue was actually one of relative simplicity. I was just missing emptying the array upon ending the search.
if didtextComplete{
print("complete")
foodApi.userSearchResults = [] //emptys list
userFoodInput = ""
}
I also marked a var didtextComplete = true when the search was finished. Not the answer I was looking for, but overall it works so I'll remain satisfied for the time being

SwiftUI: How to retrieve data from Firestore in second view?

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

runtime error when try to read fetchrequest<>

I want to get data form coredata ,and show it in Charts,
there is a runtime error at the code in init func when app is loaded:
let x=self.marks.wrappedValue.count
the error message is :Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
the whole code is following:
import SwiftUI
import Charts
import CoreData
struct CareDetails: View {
#Environment(\.managedObjectContext) var context
let timeSpan=["day","week","month","year"]
let care:Care
var marks:FetchRequest<Mark>
#State private var timeSpanIndex = 0
#State private var showSheet = false
private var sessions:[Int]=[]
private var accuracy:[Double]=[]
init(care:Care){
self.care=care
self.marks = FetchRequest(entity:Mark.entity(),
sortDescriptors: [],
predicate: NSPredicate(format:"care_id == %#",care.id!.uuidString))
let x=self.marks.wrappedValue.count
}
var body: some View {
VStack{
Picker(selection: $timeSpanIndex, label: Text("timeSpan")) {
ForEach(0..<timeSpan.count,id:\.self){ idx in
Text(self.timeSpan[idx])
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
LineChartViewSwiftUI(dataLabel: self.care.unit!,sessions: self.sessions,accuracy: self.accuracy)
Form{
Section{
List{
NavigationLink(destination:
CareDetailsList(marks: self.marks)
.environment(\.managedObjectContext, self.context)
){
Text("显示所有数据")
}
}
}
.multilineTextAlignment(.leading)
}
.padding()
}
}
}
It hasn't been evaluated yet. It resolves when update() gets called by your view's body function. Try structuring it slightly differently and using it directly in body.
struct CareView: View {
let care: Care
#FetchRequest var marks: FetchedResult<Mark>
#State private var timeSpanIndex = 0
#State private var showSheet = false
init(care: Care) {
self.care = care
self._marks = FetchRequest(...) // set up as before now. note the `_` though!
}
var body: some View {
// make sure you can ID each `Mark` by something
ForEach(marks, id: \.id) { mark in
// create your `MarkView` here
}
}
}

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.