I am new to Core Data and I'm trying to create a notes app.My project builds but when I try to add a note it crashes. I'm a newbie to using async/await, and there's not many explanations online, so I don't understand the compiler error I have.
It says "No 'async' operations occur within 'await' expression".
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Note.name, ascending: true)])
var notes: FetchedResults<Note>
#State private var NoteIds: Set<Note.ID> = []
var body: some View {
List(notes, selection: $NoteId){ note in
Text(note.name)
}
.toolbar{
ToolbarItem(placement: .primaryAction){
Button(action: newNote){
Label("New Note", systemImage: "square.and.pencil")
}
}
}
}
private func newNote(){
Task{ await newNote(name: "New Note", text:"")}
}
private func newNote(name: String, text: String) async {
await viewContext.perform { //where error is
let note = Note(context: viewContext)
note.id = UUID()
note.name = name
note.text = text
}
try? PersistenceController.shared.saveContext()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
PersistanceController:
final class PersistenceController{
static let shared = PersistenceController()
lazy var container: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Notes")
container.loadPersistentStores {description, error in
if let error = error {
fatalError("Unable to load persistent stores: \(error)")
}
}
return container
}()
private init() {}
public func saveContext(backgroundContext: NSManagedObjectContext? = nil) throws {
let context = backgroundContext ?? container.viewContext
guard context.hasChanges else {return}
try context.save()
}
}
Make sure your projects only supports
iOS 15.0+
iPadOS 15.0+
macOS 12.0+
Mac Catalyst 15.0+
tvOS 15.0+
watchOS 8.0+
Xcode 13.0+
I want to emphasize the versions async/await is Swift and is supported by more platform versions than the Core Data method.
With a basic project provided by Xcode this works in the ContentView
private func addItem() async{
do {
try await viewContext.perform {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
try viewContext.save()
}
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/3802018-perform
From WWDC Bring Core Data Concurrency to Swift and SwiftUI
The view context is on the main thread you don't need async/await. Here is the sample addItem from Xcode's Core Data app template:
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
Related
I'm trying to use CoreData in my Xcode-project (SwiftUI). I've created a Player entity and wanna use it in my View called "YouView". But when I'm trying to fetch the data, I get the error from Title. My app is called Dart Tools. My Application Language is German, so don't worry if you don't understand everything of the ui :).
Thanks for helping!
I already tried the .shared variant - same error
This is my .xcdatamodeld file (called DataModel.xcdatamodeld)
This is the Code of my DataController file:
import Foundation
import CoreData
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "DataModel")
init() {
container.loadPersistentStores { desc, error in
if let error = error {
print("Daten wurden nicht geladen: \(error.localizedDescription)")
}
}
}
func save(context: NSManagedObjectContext) {
do {
try context.save()
print("Daten wurden gespiechert!")
} catch {
print("Daten konnten nicht gespeichert werden.")
}
}
func addPlayer(name: String, isUser: Bool, context: NSManagedObjectContext) {
let player = Player(context: context)
player.id = UUID()
player.name = name
player.isUser = isUser
save(context: context)
}
func editPlayerName(player: Player, name: String, context: NSManagedObjectContext) {
player.name = name
save(context: context)
}
}
Here is my DartToolsApp.swift file:
import SwiftUI
//The error is here:
#main
struct DartToolsApp: App {
#StateObject private var userDefaults = UserDefaults()
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, DataController().container.viewContext)
.environmentObject(UserDefaults())
}
}
}
This is the file where I want to use the Data:
Btw, this view is a piece of a TabView.
import SwiftUI
import CoreData
struct YouView: View {
#EnvironmentObject var userDefaults: UserDefaults
// I guess the error is because of this line:
#FetchRequest(sortDescriptors: []) var players: FetchedResults<Player>
#Environment(\.managedObjectContext) var managedObjectContext
var body: some View {
NavigationStack {
Form {
}
.navigationTitle("\(findUser()) (Du)")
}
}
func findUser() -> String {
if let index = players.firstIndex(where: { $0.isUser }) {
let output = players[index].name!
return output
}
else {
return ""
}
}
}
This is the code to create the user:
DataController().addPlayer(name: nameText, isUser: true, context: managedObjectContext)
Wow, that was a lot of code. I hope you can help me!
You are creating new instances of DataController everywhere which is a problem in itself but what think causes your particular error is when you assign the \.managedObjectContext environment variable.
So instead of creating a new instance you should use your #StateObject instance instead.
So change this in the App code
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
I have two core data entities, a Deck and and Card. The Deck has a relationship with Card (one Deck to many Card). In SwiftUI after changing the attribute of Card in a different screen, the list of Card does not update its view to reflect the change.
struct DeckDetailView: View {
#ObservedObject var deck: Deck
#State private var showSheet : Bool = false
#Environment(\.managedObjectContext) private var viewContext
var body: some View {
Form{
ForEach(deck.cards?.allObjects as? [Card] ?? []){ card in
NavigationLink {
CardDetailView(card: card) { returnedCard in
updateCard(card: returnedCard)
}
} label: {
Text("Question: \(card.question)")
}
}
}
.navigationTitle("\(deck.name)")
.toolbar{
Button {
showSheet.toggle()
} label: {
Label("Add Deck", systemImage: "plus")
}
.sheet(isPresented: $showSheet) {
NavigationStack{
AddCardView { cardData in
addCard(cardData: cardData)
}
}
}
}
}
private func updateCard(card: Card){
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
func addCard(cardData: CardSO){
let card = Card(context: viewContext)
card.id = UUID().uuidString
card.question = cardData.question
card.answer = cardData.answer
deck.addToCards(card)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
Excellent question! I have faced this same issue many times. One of the ways, I ended up solving the issue is by using #FetchRequest property wrapper. I would pass Deck to the detail screen but use the Deck object to perform a new FetchRequest to get all the cards. When you use #FetchRequest, it will automatically track the changes.
I wrote a similar article on this issue that you can check out below:
https://azamsharp.com/2023/01/30/active-record-pattern-swiftui-core-data.html. See the section "Creating, Updating and Reading Reminders".
struct MyListDetailView: View {
let myList: MyList
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [])
private var reminderResults: FetchedResults<Reminder>
init(myList: MyList) {
self.myList = myList
_reminderResults = FetchRequest(fetchRequest: Reminder.byList(myList: myList))
}
extension Reminder: Model {
static func byList(myList: MyList) -> NSFetchRequest<Reminder> {
let request = Reminder.fetchRequest()
request.sortDescriptors = []
request.predicate = NSPredicate(format: "list = %#", myList)
return request
}
}
I am making a simple notes app. When I press a new note I want a text editor to appear for that note. I have had trouble making this run. I finally managed to build the app without errors but it crashes so clearly I didn't solve anything. I know I must have made a logic error somewhere, but I still don't understand why the app builds but crashes when I press a note.
I think the problem has to do with my NavigationLink, more specifically the destination of NoteEditor. Here is my content view:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Note.name, ascending: true)])
var notes: FetchedResults<Note>
#State private var NotesIds: Set<Note.ID> = []
private var selectedNote: Note?{
guard let NoteId = NotesIds.first,
let selectedNote = notes.filter ({$0.id == selectedNoteId}).first else{
return nil
}
return selectedNote
}
var body: some View {
NavigationView{
List(notes, selection: $NotesIds){ note in
NavigationLink(note.name, destination: NoteEditor(note: Note())) //where I think problem is
}
if let note = selectedNote{
Text(note.text)
} else{
Text("No note selected")
.foregroundColor(.secondary)
}
}
.listStyle(SidebarListStyle())
.onDeleteCommand(perform: deleteSelectedQuickNotes)
.toolbar{
ToolbarItem(placement: .primaryAction){
Button(action: createQuickNote){
Label("Create new Note", systemImage: "square.and.pencil")
}
}
}
}
private func createNote(){
createNote(name: "New note", text: "")
}
private func createNote(name: String, text: String) {
withAnimation {
let note = Note(context: viewContext)
note.id = UUID()
note.name = name
note.text = text
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteSelectedNotes(){
let selectedNotes = notes.filter {NotesIds.contains($0.id)}
deleteNotes(notes: selectedNotes)
}
private func deleteNotes(notes: [Note]){
viewContext.perform{ notes.forEach(viewContext.delete)}
}
}
Note Editor:
struct QuickNoteEditor: View {
#Environment(\.managedObjectContext) var viewContext
#ObservedObject var note: Note
var body: some View {
TextEditor(text:$note.text)
.onReceive(note.publisher(for: \.text), perform: setName)
.onReceive(
note.publisher(for: \.text)
.debounce(for: 0.5, scheduler: RunLoop.main)
.removeDuplicates()
){ _ in
try? PersistenceController.shared.saveContext()
}
.navigationTitle(note.name)
}
func setName(from text: String){
let text = text.trimmingCharacters(in: .whitespacesAndNewlines)
if text.count > 0{
note.name = String(text.prefix(20))
} else {
note.name = "New Note";
}
}
}
Persistence Controller (not sure if relevant)
final class PersistenceController{
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Notes")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { description, error in
if let error = error {
fatalError("cannot load data \(error)")
}
}
}
public func saveContext(backgroundContext: NSManagedObjectContext? = nil) throws{
let context = backgroundContext ?? container.viewContext
guard context.hasChanges else { return}
try context.save()
}
}
NoteEditor(note: Note()) should be NoteEditor(note: note)
Interesting technique trying to save in onReceive it might break if the context fails to save and gets in an inconsistent state.
I trying to save a Coredata object in a SwiftUI .OnDisappear method and it conflicts with a Date() object in the View. Commenting out the Date() variable in DetailView or commenting out the code in .OnDisappear stops the view from disappearing. I made a minimum reproducible example here from the default New Project with Coredata in Xcode.
Here is a gif of the DetailView disappearing
https://imgur.com/dA2QH4D
ListView
struct ListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: DetailView(item: item), label: {
Text(item.timestamp!, formatter: itemFormatter)
})
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
DetailView
struct DetailView: View {
#State var item: Item
#State private var date = Date() // Comment this
var body: some View {
VStack {
Text(item.timestamp!, formatter: itemFormatter)
NavigationLink(destination: {
EmptyView()
}, label: {
Text("History")
})
}
.onDisappear {
item.timestamp = Date() // Or Comment this
}
}
}
In ListView, add a .navigationViewStyle(.stack) to your NavigationView, such as:
NavigationView {
//...
}.navigationViewStyle(.stack) // <-- here
NavigationView can only have one level of detail NavigationLink i.e. its isDetail property defaults to true. If you want more levels you have to use .isDetail(false) on the 2nd level and beyond.
I think I'm going about this SwiftUI thing all wrong. It's clear that we're just defining the layout as a structs and there can be limited conventional programming embroiled in the layout. I'm having difficulties thinking like this. What is the best way of doing this?
Take the example below. Project is an NSManagedObject. All I want to do is pass in example record so the SwiftUI will render. Nothing I try works.
struct ProjectView: View
{
#State var project: Project //NSManagedObject
var body: some View
{
TextField("", text: Binding<String>($project.projectName)!)
}
}
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
var p:Project
p = getFirstProject() //returns a Project
return ProjectView(project: p)
}
}
If I try returning the struct it says it cannot preview in the file.
If I don't return the struct I get a Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type error.
UPDATE:
var app = UIApplication.shared.delegate as! AppDelegate
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var persistentContainer: NSPersistentCloudKitContainer = {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var managedObjectContext: NSManagedObjectContext =
{
return persistentContainer.viewContext
}()
}
And the rest of the code:
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func getCount() -> String
{
let r = allRecords(Project.self)
return String(r.count)
}
struct ProjectView: View
{
// #ObservedObject var project: Project
var body: some View
{
Text(getCount())
// TextField("", text: Binding<String>($project.projectName)!)
}
}
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
ProjectView()
}
}
r.count is returning 0, but in the main application thread it is returning 8. Has app.managedObjectContext not been defined properly? I think this has just got too complicated too quickly.
Assuming getFirstProject works correctly the following should work
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
ProjectView(project: getFirstProject())
}
}
However there are concerns about the following...
struct ProjectView: View
{
#State var project: Project //NSManagedObject
because #State is designed to be internal view state-only thing, but Project in your case is a model, so the recommended scenario for this is to use ObservableObject view model either by conforming Project or as standalone clue class holding Project instance(s).