TextField Xcode 11 Binding String problem - string

I have a file called PDFManager , which is in charge to create and save the pdf file.
on PDFManager I create the var nameCPT : String = "" and from the ContentView i'm try to fill this var with a value from a textfield
problem is, Xcode give me the error say "Cannot convert value of type 'String' to expected argument type 'Binding'"
I tried on pdf manager create a var with #State but still not working
any idea how to pass the value of my textField to PDFManager?
thanks
import SwiftUI
struct ContentView: View {
var lm : PDFManager
var body: some View {
VStack {
fakebar
Spacer()
HStack {
TextField("Insert Nome CPT", text: lm.nameCPT).
// not working, I try with $lm.nameCPT but still not working
.padding(.leading)
}
Spacer()
}
}

You need to use #Observed property wrapper to allow the property being observed and comply with ObservableObject in your PDFManager class.
class PDFManager:ObservableObject {
var nameCPT:String = "test"
}
struct ContentView: View {
#ObservedObject var lm : PDFManager = PDFManager()
var body: some View {
VStack {
Spacer()
HStack {
TextField("Insert Nome CPT", text: $lm.nameCPT)
.padding(.leading)
Button(action:{
// Prints the value stored in your PDF manager
print(self.lm.nameCPT)
}){
Text("Check")
}
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

swiftUI - how to pass core data values of an item from ForEach list in NavigationLink to Detail view

I just cannot figure out how to pass core data values of an item from ForEach list in NavigationLink to Detail view. Here is the code that got error: "Cannot convert value of type 'FetchedResults.Element' (aka 'FileEnt') to expected argument type 'FileViewModel'"
struct FileList: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: FileEnt.entity(), sortDescriptors: [NSSortDescriptor(key: "fileName", ascending: false)]) var results: FetchedResults<FileEnt>
#ObservedObject var fileData : FileViewModel
var body: some View {
NavigationView {
List {
ForEach(results) { aFile in
NavigationLink(destination: FileDetails(fileData: aFile), label: {
// ** error on red-underscored aFile above.
Text(aFile.fileName ?? "")
})
}
}.navigationBarTitle("Files")
}
}
}
FileViewModel is for Add, Edit and Detail views. Here is its simplified version for the question:
class FileViewModel: ObservableObject {
#Published var fileName = ""
init(){
}
func DetailItem(fileItem: FileEnt){
fileName = fileItem.fileName ?? ""
}
}
FileDetails:
struct FileDetails: View {
#ObservedObject var fileData : FileViewModel
#Environment(\.managedObjectContext) var context
#State var isEdit = false
var body: some View {
VStack {
Form {
HStack {
Text("File Name:")
Spacer()
Text(fileData.fileName)
....
Modified the post for easy understanding. Thanks for any advice.

Would like to store system settings in core data

Using SwiftUI
I have created a CoreData Entity called SystemSettings and would like to use the information stored in it for calculating results in different functions on different views without using ForEach. SystemSettings will always have only one object (Record) stored from preloaded database. Is there a way I can use #FetchRequest and pull out the only object and its attributes out, as a var.... ?
Thank you
import SwiftUI
import CoreData
struct NewComponent: View {
#FetchRequest(entity: SystemSettings.entity(), sortDescriptors: []) var settings: FetchedResults<SystemSettings>
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
var calculator = Calculator()
// USER DATA TEXT FIELDS
#State var parameterOne = "12.5"
#State var parameterTwo = "2.5"
#State var calculationResult = ""
var body: some View {
VStack {
Form {
Section (header: Text("Parameters").font(.headline).bold().italic()) {
HStack {
Text ("Length")
.font(.headline)
Spacer()
TextField ("m", text: $parameterOne).modifier(ClearButton(text: $parameterOne))
.multilineTextAlignment(.trailing)
.keyboardType(.decimalPad)
.font(.headline)
}
HStack {
Text ("Width")
.font(.headline)
Spacer()
TextField ("m", text: $parameterTwo).modifier(ClearButton(text: $parameterTwo))
.multilineTextAlignment(.trailing)
.keyboardType(.decimalPad)
.font(.headline)
}
}
}
Section(header:
Text("\(self.calculationResult)").bold().italic().padding(.top)
) {
Button(action: {
self.calculationResult = self.result()
}) {
HStack {
Spacer()
Image(systemName: "x.squareroot")
Text("Calculate!")
Spacer()
}.font(.headline).foregroundColor(Color.green)
.padding(.bottom, 40.0).padding(.top, 20.0)
}
}
}.navigationBarTitle(Text("Component Calculator"))
.gesture(DragGesture().onChanged{_ in UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)})
.modifier(KeyboardObserving())
.navigationBarItems(trailing: Button(action: {
let newEstimatedComponent = EstimatedComponents(context: self.moc)
newEstimatedComponent.name = "New Component"
newEstimatedComponent.price = self.priceToDisplay()
try? self.moc.save()
self.presentationMode.wrappedValue.dismiss()
}){Image(systemName: "checkmark.circle").font(.system(size: 30))})
}
func result () -> String {
let result = componentCost()
return (String(format: "%.2f",(result)) + " €")
}
func priceToDisplay() -> Double {
return componentCost()
}
func componentCost () -> Double {
return calculator (componentWidth: Double(parameterOne), componentHight: Double(parameterTwo), componentPrice: settings!.componentM2Price )
}
/*settings!componentM2Price not working of course, so I need to get the first and the only object from SystemSettings Entity in a way that I can use it in func componentCost()...*/
THANKS AGAIN...
Solved!
#EnvironmentObject var settings: ModifySystemSettings
let systemSettings: SystemSettings
You can find more information here:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views

Passing Core Data FetchedResults<T> for previews in SwiftUI

I have a parent view which does a #FetchRequest and passes the FetchedResults<T> to a child view. Everything works, and the child view is able to parse through the FetchedResults. However, I can't figure out how to set up the data so that the child's Preview struct will work. What's the proper way to set up some constant data in Preview struct so that I can instantiate the child view and pass in FetchedResults<T>?
As FetchedResults<T> is a RandomAccessCollection and swift array also is a RandomAccessCollection, here is possible solution.
Update: verified with Xcode 13.3 / iOS 15.4
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: Person.entity(), sortDescriptors: [])
var result: FetchedResults<Person>
var body: some View {
VStack(alignment: .leading) {
Text("Persons").font(.title)
PersonsView(results: result) // FetchedResults<Person> is a collection
}
}
}
// generalize PersonsView to depend just on collection
struct PersonsView<Results:RandomAccessCollection>: View where Results.Element == Person {
let results: Results
var body: some View {
ForEach(results, id: \.self) { person in
Text("Name: \(person.name ?? "<unknown>")")
}
}
}
// Tested with Xcode 11.4 / iOS 13.4
// DOES NOT WORK ANYMORE !!
// struct ChildView_Previews: PreviewProvider {
// static var previews: some View {
// PersonsView(results: [Person()]) // << use regular array //to test
// }
}
Update: fixed & tested part for Xcode 12 / iSO 14 (due to crash of above PreviewProvider)
It appears entity now should be read & specified explicitly:
struct ChildView_Previews: PreviewProvider {
static let entity = NSManagedObjectModel.mergedModel(from: nil)?.entitiesByName["Person"]
static var previews: some View {
let person = Person(entity: entity!, insertInto: nil)
person.name = "Test Name"
return PersonsView(results: [person])
}
}
Use the preview PersistenceController within your pre-generated PersistenceController struct (inside the "Persistence.swift" file).
so if you pass an item from a Core Data "Item"-entity:
struct ContentView: View {
...
private var items: FetchedResults<Item>
..
ForEach(items) { item in
DetailView(item: item)
}
..
In the Detail-View go like this:
struct DetailView: View {
var item: FetchedResults<Item>.Element
var body: some View {
Text("Items text = \(item.text ?? "")")
}
}
struct Detail_Previews: PreviewProvider {
static var previews: some View {
let viewContext = PersistenceController.preview.container.viewContext
let previewItem = Item(context: viewContext)
previewItem.text = "Text4preview"
return Detail(item: previewItem)
}
}

How to update #FetchRequest, when a related Entity changes in SwiftUI?

In a SwiftUI View i have a List based on #FetchRequest showing data of a Primary entity and the via relationship connected Secondary entity.
The View and its List is updated correctly, when I add a new Primary entity with a new related secondary entity.
The problem is, when I update the connected Secondary item in a detail view, the database gets updated, but the changes are not reflected in the Primary List.
Obviously, the #FetchRequest does not get triggered by the changes in another View.
When I add a new item in the primary view thereafter, the previously changed item gets finally updated.
As a workaround, i additionally update an attribute of the Primary entity in the detail view and the changes propagate correctly to the Primary View.
My question is:
How can I force an update on all related #FetchRequests in SwiftUI Core Data?
Especially, when I have no direct access to the related entities/#Fetchrequests?
import SwiftUI
extension Primary: Identifiable {}
// Primary View
struct PrimaryListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(
entity: Primary.entity(),
sortDescriptors: [NSSortDescriptor(key: "primaryName", ascending: true)]
)
var fetchedResults: FetchedResults<Primary>
var body: some View {
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(primary: primary)) {
VStack(alignment: .leading) {
Text("\(primary.primaryName ?? "nil")")
Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
}
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
private func addNewPrimary() {
let newPrimary = Primary(context: context)
newPrimary.primaryName = "Primary created at \(Date())"
let newSecondary = Secondary(context: context)
newSecondary.secondaryName = "Secondary built at \(Date())"
newPrimary.secondary = newSecondary
try? context.save()
}
}
struct PrimaryListView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return NavigationView {
PrimaryListView().environment(\.managedObjectContext, context)
}
}
}
// Detail View
struct SecondaryView: View {
#Environment(\.presentationMode) var presentationMode
var primary: Primary
#State private var newSecondaryName = ""
var body: some View {
VStack {
TextField("Secondary name:", text: $newSecondaryName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.onAppear {self.newSecondaryName = self.primary.secondary?.secondaryName ?? "no name"}
Button(action: {self.saveChanges()}) {
Text("Save")
}
.padding()
}
}
private func saveChanges() {
primary.secondary?.secondaryName = newSecondaryName
// TODO: ❌ workaround to trigger update on primary #FetchRequest
primary.managedObjectContext.refresh(primary, mergeChanges: true)
// primary.primaryName = primary.primaryName
try? primary.managedObjectContext?.save()
presentationMode.wrappedValue.dismiss()
}
}
I also struggled with this and found a very nice and clean solution:
You have to wrap the row in a separate view and use #ObservedObject in that row view on the entity.
Here's my code:
WineList:
struct WineList: View {
#FetchRequest(entity: Wine.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Wine.name, ascending: true)
]
) var wines: FetchedResults<Wine>
var body: some View {
List(wines, id: \.id) { wine in
NavigationLink(destination: WineDetail(wine: wine)) {
WineRow(wine: wine)
}
}
.navigationBarTitle("Wines")
}
}
WineRow:
struct WineRow: View {
#ObservedObject var wine: Wine // !! #ObserveObject is the key!!!
var body: some View {
HStack {
Text(wine.name ?? "")
Spacer()
}
}
}
You need a Publisher which would generate event about changes in context and some state variable in primary view to force view rebuild on receive event from that publisher.
Important: state variable must be used in view builder code, otherwise rendering engine would not know that something changed.
Here is simple modification of affected part of your code, that gives behaviour that you need.
#State private var refreshing = false
private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
var body: some View {
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(primary: primary)) {
VStack(alignment: .leading) {
// below use of .refreshing is just as demo,
// it can be use for anything
Text("\(primary.primaryName ?? "nil")" + (self.refreshing ? "" : ""))
Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
}
}
// here is the listener for published context event
.onReceive(self.didSave) { _ in
self.refreshing.toggle()
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
An alternative method: using a Publisher and List.id():
struct ContentView: View {
/*
#FetchRequest...
*/
private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave) //the publisher
#State private var refreshID = UUID()
var body: some View {
List {
...
}
.id(refreshID)
.onReceive(self.didSave) { _ in //the listener
self.refreshID = UUID()
print("generated a new UUID")
}
}
}
Every time you call save() of NSManagedObjects in a context, it genertates a new UUID for the List view, and it forces the List view to refresh.
To fix that you have to add #ObservedObject to var primary: Primary in SecondaryView to work List properly. Primary belong to NSManagedObject class, which already conforms to #ObservableObject protocol. This way the changes in instances of Primary are observed.
import SwiftUI
extension Primary: Identifiable {}
// Primary View
struct PrimaryListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(
entity: Primary.entity(),
sortDescriptors: [NSSortDescriptor(key: "primaryName", ascending: true)]
)
var fetchedResults: FetchedResults<Primary>
var body: some View {
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(primary: primary)) {
VStack(alignment: .leading) {
Text("\(primary.primaryName ?? "nil")")
Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
}
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
private func addNewPrimary() {
let newPrimary = Primary(context: context)
newPrimary.primaryName = "Primary created at \(Date())"
let newSecondary = Secondary(context: context)
newSecondary.secondaryName = "Secondary built at \(Date())"
newPrimary.secondary = newSecondary
try? context.save()
}
}
struct PrimaryListView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return NavigationView {
PrimaryListView().environment(\.managedObjectContext, context)
}
}
}
// Detail View
struct SecondaryView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var primary: Primary
#State private var newSecondaryName = ""
var body: some View {
VStack {
TextField("Secondary name:", text: $newSecondaryName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.onAppear {self.newSecondaryName = self.primary.secondary?.secondaryName ?? "no name"}
Button(action: {self.saveChanges()}) {
Text("Save")
}
.padding()
}
}
private func saveChanges() {
primary.secondary?.secondaryName = newSecondaryName
try? primary.managedObjectContext?.save()
presentationMode.wrappedValue.dismiss()
}
}
I tried to touch the primary object in the detail view like this:
// TODO: ❌ workaround to trigger update on primary #FetchRequest
if let primary = secondary.primary {
secondary.managedObjectContext?.refresh(primary, mergeChanges: true)
}
Then the primary list will update. But the detail view has to know about the parent object. This will work, but this is probably not the SwiftUI or Combine way...
Edit:
Based on the above workaround, I modified my project with a global save(managedObject:) function. This will touch all related Entities, thus updating all relevant #FetchRequest's.
import SwiftUI
import CoreData
extension Primary: Identifiable {}
// MARK: - Primary View
struct PrimaryListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(
sortDescriptors: [
NSSortDescriptor(keyPath: \Primary.primaryName, ascending: true)]
)
var fetchedResults: FetchedResults<Primary>
var body: some View {
print("body PrimaryListView"); return
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(secondary: primary.secondary!)) {
VStack(alignment: .leading) {
Text("\(primary.primaryName ?? "nil")")
Text("\(primary.secondary?.secondaryName ?? "nil")")
.font(.footnote).foregroundColor(.secondary)
}
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
private func addNewPrimary() {
let newPrimary = Primary(context: context)
newPrimary.primaryName = "Primary created at \(Date())"
let newSecondary = Secondary(context: context)
newSecondary.secondaryName = "Secondary built at \(Date())"
newPrimary.secondary = newSecondary
try? context.save()
}
}
struct PrimaryListView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return NavigationView {
PrimaryListView().environment(\.managedObjectContext, context)
}
}
}
// MARK: - Detail View
struct SecondaryView: View {
#Environment(\.presentationMode) var presentationMode
var secondary: Secondary
#State private var newSecondaryName = ""
var body: some View {
print("SecondaryView: \(secondary.secondaryName ?? "")"); return
VStack {
TextField("Secondary name:", text: $newSecondaryName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.onAppear {self.newSecondaryName = self.secondary.secondaryName ?? "no name"}
Button(action: {self.saveChanges()}) {
Text("Save")
}
.padding()
}
}
private func saveChanges() {
secondary.secondaryName = newSecondaryName
// save Secondary and touch Primary
(UIApplication.shared.delegate as! AppDelegate).save(managedObject: secondary)
presentationMode.wrappedValue.dismiss()
}
}
extension AppDelegate {
/// save and touch related objects
func save(managedObject: NSManagedObject) {
let context = persistentContainer.viewContext
// if this object has an impact on related objects, touch these related objects
if let secondary = managedObject as? Secondary,
let primary = secondary.primary {
context.refresh(primary, mergeChanges: true)
print("Primary touched: \(primary.primaryName ?? "no name")")
}
saveContext()
}
}
If you are here, i don't find the reason why your view isn't updating, i think this will help you:
Always use the #ObservedObject when you declare a core data type.
If you are using MVVM, wrap the view model also with #ObservedObject, and in the VM create the core data type with #Published.
This is an example of creating a VM with #ObservedObject, so when core data receives the update, the instance of the view model recreate itself, and the view is updated.
class ProductTitleValueViewModel: BaseViewModel, ObservableObject {
// MARK: - Properties
#Published var product: Product
var colorSet: [Color]
var currency: Currency
// MARK: - Init
init(product: Product, colorSet: [Color], currency: Currency) {
self.product = product
self.colorSet = colorSet
self.currency = currency
}
}
struct ProductTitleValueView: View {
#ObservedObject var viewModel: ProductTitleValueViewModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
HStack {
Circle()
.fill(
LinearGradient(colors: viewModel.colorSet, startPoint: .leading, endPoint: .trailing)
)
.opacity(0.6)
.frame(width: 20, height: 20)
Text(viewModel.product.wrappedName)
.font(.callout.bold())
.foregroundColor(ThemeColor.lightGray)
}
Text(viewModel.product.balance.toCurrency(with: viewModel.currency))
.font(.callout.bold())
.padding(.leading, 28)
}
}
}
If you follow this 2 simple things, you are not going to have problem with core date updating your views.

SwiftUI TextField CoreData - Changing an attribute's data

I'm trying to use TextField to change the data of an attribute of CoreData, and everything I've come up with hasn't been successful. There is a similar question (listed below), and I'm going to post the code from the correct answer to that to explain it.
struct ItemDetail: View {
#EnvironmentObject var itemStore: ItemStore
let idx: Int
var body: some View {
NavigationView {
Stepper(value: $itemStore.items[idx].inventory) {
Text("Inventory is \(self.itemStore.items[idx].inventory)")
}
// Here I would like to do this
// TextField("PlaceHolder", $itemStore.items[idx].name)
// That doesn't work... also tried
// TextField("PlaceHolder", $name) - where name is a #State String
// How can you then automaticlly assign the new value of #State name
// To $itemStore.items[idx].name?
.padding()
.navigationBarTitle(itemStore.items[idx].name)
}
}
}
Original Question:
SwiftUI #Binding doesn't refresh View
I now have it working.
struct ItemDetail: View {
#EnvironmentObject var itemStore: ItemStore
let idx: Int
// Added new #State variable
#State var name = ""
var body: some View {
NavigationView {
Stepper(value: $itemStore.items[idx].inventory) {
Text("Inventory is \(self.itemStore.items[idx].inventory)")
}
TextField("Placeholder", text: $name) {
// When the enter key is tapped, this runs.
self.itemStore.items[self.idx].name = self.name
}
.padding()
.navigationBarTitle(itemStore.items[idx].name)
}
}
}

Resources