I am not able to bind the bookName from my Core Data object to a TextField inside a ForEach loop. How can I get this binding to work? I want the bookName value to be saved to Core Data when it changes.
I am receiving an error that says: Cannot find $book in scope.
extension Book: Identifiable {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Book> {
return NSFetchRequest<Book>(entityName: "Book")
}
#NSManaged public var id: UUID?
#NSManaged public var bookName: String?
var wrappedBookName: String {
bookName ?? ""
}
}
struct BookListView: View {
#FetchRequest(entity: Book.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Book.rankNumber, ascending: false)]) var books: FetchedResults<Book>
var body: some View {
ForEach(books) { book in
TextField("Book Name", text: $book.bookName) //Error: cannot find $book in scope
}
}
}
I love CoreData with SwiftUI. It works just so simple. Just create a new edit view with ObservedObject of Book.
struct EditView : View {
#ObservedObject var book: Book
init(book: Book) {
self.book = book
}
var body : some View {
TextField("Name", text: $book.bookName)
}
}
And that's it. Then you can send $book.bookName as Binding to the String.
However, make sure! you have declared bookName as non-optional value in CoreData. TextField requires a Binding<String>, NOT a Binding<String?>
Use that EditView inside your ForEach and you are ready to go:
ForEach(books) { book in
EditView(book: book)
Related
I am trying to create a Recpipe object (CoreData entity) and then pass that object to a ChildView after it's created. What's the best way to do this?
My first attempt was to create a #StateObject in the MainView and then pass that to ChildViews as an #ObservedObject, but this seems to only work great for objects that already exist. The #StateObject is a 'get only' property so I can't modify it based on the function return. Perhaps I don't really want to create a #StateObject in this circumstance?
struct MainView: View {
#State private var presentChildView: Bool = false
#StateObject private var recipe: Recipe = Recipe()
var body: some View {
VStack {
NavigationLink(destination: ChildView(recipe: recipe), isActive: $presentChildView) {
EmptyView()
}
Button("Action", action: {
recipe = functionThatReturnsRecipe()
presentChildView = true
})
}
}
}
struct ChildView: View {
#ObservedObject private var recipe: Recipe
var body: some View {
Text(recipe.title)
}
}
Normally you hold the object in #State or even better inside a struct to hold all the ChildView's related vars so it can be tested independently, e.g.
struct ChildViewConfig {
var recipe: Recipe?
var isPresented = false
mutating func present(viewContext: NSManagedObjectContext) {
recipe = Recipe(context: viewContext)
isPresented = true
}
// might have other save or dismiss mutating funcs here.
}
struct MainView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var config = ChildViewConfig()
var body: some View {
VStack {
Button("Present") {
config.present(context: viewContext)
}
}
.sheet(isPresented: $config.isPresented, onDismiss: nil) {
ChildView(recipe: config.recipe!)
}
}
}
struct ChildView: View {
#ObservedObject private var recipe: Recipe
var body: some View {
Text(recipe.title)
}
}
This answer to another question uses the more advanced .sheet(item:onDismiss) that you may prefer because is designed for this use case of editing an item and allows for the use of a child context scratch pad because when setting the item to nil means the item.childContext will be discarded if the editing in the sheet is cancelled.
Learn more about this #State Config struct pattern at 4:18 in Data Essentials in SwiftUI (WWDC 2020)
You can have instead some concept of app state or manager (anything represented business logic) which would hold/update your recipes, like
class AppState: ObservableObject {
#Published var recipe: Recipe = Recipe()
func refreshRecipe() {
self.recipe = functionThatReturnsRecipe() // << now works !!
}
private func functionThatReturnsRecipe() -> Recipe {
Recipe()
}
}
struct MainView: View {
#State private var presentChildView: Bool = false
#StateObject private var appState = AppState() // << non-changeable
var body: some View {
VStack {
NavigationLink(destination: ChildView(recipe: appState.recipe), isActive: $presentChildView) {
EmptyView()
}
Button("Action", action: {
appState.refreshRecipe()
presentChildView = true
})
}
}
}
I'm trying to use a ViewModel between the ContentView and Core Data in SwiftUI. Xcode builder runs the App but I get an immediate error: Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) for var recList.
Can anyone help?
Following a simple example of what I'm doing:
ListViewModel:
class ListViewModel: ObservableObject {
var recRequest: FetchRequest<Newdb>
var recList: FetchedResults<Newdb>{recRequest.wrappedValue} <-------- error appears here
#Published var records = [ViewModel]()
init() {
self.recRequest = FetchRequest(entity: Newdb.entity(), sortDescriptors: [])
fetchEntries()
}
func fetchEntries() {
self.records = recList.map(ViewModel.init)
}
}
ViewModel:
class ViewModel {
var name: String = ""
init(db: Newdb) {
self.name = db.name!
}
}
ContentView:
struct ContentView: View {
#ObservedObject var listViewModel: ListViewModel
init() {
self.listViewModel = ListViewModel()
}
var body: some View {
ForEach(listViewModel.records, id: \.name) { index in
Text(index.name)
}
}
}
two things I noticed; your ListViewModel is an ObservableObject but you do not have any #Published var ...
Also when creating a class such as ListViewModel you cannot use "recRequest" as you do in recList, because it is not created yet. It is created in the init() method not before.
Do your "recList = FetchedResults{recRequest.wrappedValue}" somewhere else, like in the fetchEntries().
From what I can tell, FetchRequest is a property wrapper.
It is supposed to wrap something, e.g.;
#FetchRequest(
entity: User.entity(),
sortDescriptors: []
) var users: FetchedResults<User> // users are 'wrapped' in a FetchRequest instance
It makes sense that wrappedValue is nil because there's nothing to be wrapped in
self.recRequest = FetchRequest(entity: Newdb.entity(), sortDescriptors: [])
You might want to double-check its usage.
I have a view class showing list of items coming from ViewModel class, in picker. Initial state of this picker is first element from the array of objects of the viewModel class.
On selection of item from picker, I want to do different actions in that view - 1. send the object info to different screen on button click. 2. display information with respected to selected object from picker.
import SwiftUI
import Combine
struct SetConfiguration: View {
#ObservedObject var profileListVM : ProfilesListViewModel = ProfilesListViewModel()
#State private var selectedConfiguration = 0 ///show "Add" as initial state
var body: some View {
HStack {
Text("Configuration:")
Picker(selection: $selectedConfiguration.onChange(connectToConfiguration), label: EmptyView()) {
ForEach(profileListVM.profiles, id: \.self) {
choice in
Text(choice.name).tag(choice)
}
}
Text (“Selcted item is: \(self. selectedconfiguration.name)”)
Button(action: {
}) {
Text("Edit")
}.sheet(isPresented: $showEditConfig) {
EditConfigurationView()
// TODO pass selectedConfiguration as profile object
}
}
}
viewModel class:
class ProfilesListViewModel: ObservableObject {
#Published var profiles: [ProfileViewModel] = [ProfileViewModel]()
static var addNewProfile = ProfileViewModel(name: "Add Configuration")
init() {
fetchAllProfiles()
}
func fetchAllProfiles() {
profiles.append(ProfilesListViewModel.addNewProfile) ///Add is first object
self.profiles = CoreDataManager.shared.getConfigurations().map(ProfileViewModel.init) /// fetch all profile objects
}
}
I believe this is the context for your question. Here is the working example:
// MARK: MOCKS FOR MODELS
struct ProfileViewModel: Hashable {
let id = UUID()
let name: String
}
class CoreDataManager {
static let shared = CoreDataManager()
func getConfigurations() -> [ProfileViewModel] {
return [ProfileViewModel(name: "first"), ProfileViewModel(name: "second"), ProfileViewModel(name: "third")]
}
}
// MARK: changed class because it's not even working because of lack of code
class ProfilesListViewModel: ObservableObject {
#Published var profiles: [ProfileViewModel] = [ProfileViewModel]()
static var addNewProfile = ProfileViewModel(name: "Add Configuration")
init() {
fetchAllProfiles()
}
func fetchAllProfiles() {
print("fetched")
profiles.append(ProfilesListViewModel.addNewProfile) ///Add is first object
self.profiles = CoreDataManager.shared.getConfigurations()
}
}
// MARK: the solution
struct SetConfiguration: View {
#ObservedObject var profileListVM: ProfilesListViewModel = ProfilesListViewModel()
#State private var selectedConfiguration = 0 ///show "Add" as initial state
#State private var choosedConfiguration = 0
var body: some View {
VStack {
HStack {
Picker(selection: $selectedConfiguration.onChange(selectNewConfig), label: Text("Configuration")) {
ForEach(0 ..< self.profileListVM.profiles.count) { choice in
Text(self.profileListVM.profiles[choice].name).tag(choice)
}
}
}
Text("Selected item is: \(choosedConfiguration)")
}
}
func selectNewConfig(_ newValue: Int) {
print(newValue)
withAnimation {
choosedConfiguration = newValue
}
}
}
Tips
To avoid misunderstandings in the future:
you should add all the working code and links, or simplify it to be clear what you want to achieve. Not every swift developer know about extension Binding, so they will just say: onChange will not ever work and they will be right;
format your code;
add some examples of your models or remove/simplify them.
I believe, you don't need choosedConfiguration, you can do some experiments with this.
I make a list for audio items from coredata. after deleting, crash reported as "EXC_BREAKPOINT (code=1, subcode=0x1b8fb693c)", why?
When using
ForEach(items, id: \.self)
, it works. But My Audio has id property and follow Identifiable protocol.
UPDATE: I found adding a if{} clause will fix crash, but why? Breakpoint at "static UUID.unconditionallyBridgeFromObjectiveC(:) ()".
struct Test1View: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: Audio.fetchAllAudios()) var items: FetchedResults<Audio>
var body: some View {
List {
ForEach(items) { item in
if true { // <- this if clause fix crash, but why?
HStack {
Text("\(item.name)")
}
}
}.onDelete(perform: { indexSet in
let index = indexSet.first!
let item = self.items[index]
self.context.delete(item)
try? self.context.save()
})
}
}
}
code as following:
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID
#NSManaged public var name: String
#NSManaged public var createAt: Date
}
struct Test1View: View {
#Environment(\.managedObjectContext) var context
var fetchRequest: FetchRequest<Audio> = FetchRequest<Audio>(entity: Audio.entity(), sortDescriptors: [NSSortDescriptor(key: "createAt", ascending: false)])
var items: FetchedResults<Audio> { fetchRequest.wrappedValue }
var body: some View {
List {
ForEach(items) { item in
HStack {
Text("\(item.name)")
}
}.onDelete(perform: { indexSet in
let index = indexSet.first!
let item = self.items[index]
self.context.delete(item)
try? self.context.save()
})
}
}
}
I had the same problem as you.
Probably it is a bad idea to use your own id property to make it Identifiable because Core Data is setting all the properties to nil when the object is deleted, even if you declared it differently in your Swift CoreData class.
When deleting your entity, the id property gets invalidated and the objects .isFault property is set to true, but the SwiftUI ForEach still holds some reference to this ID object (=your UUID) to be able to calculate the "before" and "after" state of the list and somehow tries to access it, leading to the crash.
Therefore the following recommendations:
Protect the detail view (in the ForEach loop by checking isFault:
if entity.isFault {
EmptyView()
}
else {
// your regular view body
}
Expect your id property to be nil, either by defining it accordingly in your core data model as optional
#NSManaged public var id: UUID?
or by not relying on the Identifiable protocol in the SwiftUI ForEach loop:
ForEach(entities, id: \.self) { entity in ... }
or
ForEach(entities, id: \.objectID) { entity in ... }
Conclusion: you really do not need to make all your CoreData properties Swift Optionals. It's simply important that your id property referenced in the ForEach loop handles the deletion (=setting its value to nil) gracefully.
I found the reason of crash, must provide optional, because of OC/swift object conversion:
convert
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID
#NSManaged public var name: String
#NSManaged public var createAt: Date
}
to
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID?
#NSManaged public var name: String?
#NSManaged public var createAt: Date?
}
I had the same issue over the weekend. It looks like SwiftUI want's to unwrap the value i read from CoreData and as the value is already deleted it crashes.
In my case i did solve it with nil coalescing on all values i use from CoreData.
You can try to provide a default value on your item.name with
ForEach(items) { item in
HStack {
Text("\(item.name ?? "")")
}
}
Is it possible to use a core data record in a predicate inside the #FetchRequest property wrapper in SwiftUI?
I have a list of Project and a list of Tasks. I want to tap on a project and navigate to a list of related tasks for that project. I can't seem to find a way to pass in the parent project in a way that SwiftUI can see before the #FetcheRequest is initialized.
I tried placing the parent project in an EnvironmentObject. This is called when I navigate from the ProjectListView to the TaskListView.
TaskListView()
.environment(\.managedObjectContext, self.managedObjectContext)
.environmentObject(self.projectToEdit)
Then in the TaskListView I added tried this:
#Environment(\.managedObjectContext) var managedObjectContext
#EnvironmentObject var parentProject: Project
#FetchRequest(
entity: Task.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Task.name, ascending: true)
],
predicate: NSPredicate(format: String(format: "%#%#", "taskProject", " == %#"), parentProject)
) var tasks: FetchedResults<Task>
I get the following error on the line with the predicate.
Cannot use instance member 'parentProject' within property initializer; property initializers run before 'self' is available
So is there a way to write a predicate in some way that can use the parent project? Passing the project to the task view does not seem like it's going to work. How else would I go about using a record in a predicate like this?
The FetchRequest can be dynamically created in the init method. That way you can vary predicate and sort conditions. Here is some sample code to achieve that.
// sample Project class
class Project:NSManagedObject {
var id : String
var name : String
}
// sample Task class
class Task:NSManagedObject {
var id : String
var prjId : String
var name : String
}
// Task List View
struct TaskListView: View {
#Environment(\.managedObjectContext) var managedObjectContext
private var tasksRequest: FetchRequest<Task>
private var tasks: FetchedResults<Task> { tasksRequest.wrappedValue }
private var project:Project
// init Task with Project
init(_ project:Project) {
self.project = project
// create FetchRequest
self.tasksRequest = FetchRequest(
entity: Task.entity(),
sortDescriptors: [NSSortDescriptor(key: "name", ascending:true)],
predicate: NSPredicate(format: "prjId == %#", project.id))
}
var body: some View {
VStack {
Section(header: Text("Tasks under \(project.name):")) {
// access the fetched objects
ForEach(tasks, id:\.id) { task in
Text("\(task.name)")
}
}
}
}
}
Then the call to TaskListView() would look like:
// call to TaskListView
TaskListView(self.projectToEdit)
.environment(\.managedObjectContext, self.managedObjectContext)