Sending an NSManagedObjectID to a struct / view - macos

I'm complete new to swift, swiftui and coredata. I have good programming experience in other languages, but swift is its own world. :-)
Important information: it's for macOS not iOS!!
My problem: I want to edit a Dataset in an separate view displayed in a sheet. I followed this example (SwiftUI update view on core data object change), but when trying to run, my NSManagedObjectID is allway nil.
The ContentView (shortened)
import SwiftUI
import CoreData
struct ContentView: View {
#State public var selectedBookId: NSManagedObjectID?
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Books.title, ascending: true)],
animation: .default)
private var books: FetchedResults<Books>
#State private var showingEditScreen = false
var body: some View {
NavigationView {
List {
ForEach(books, id: \.self) { book in
HStack {
NavigationLink {
HStack {
Button {
// here store objectID to var
selectedBookId = book.objectID
showingEditScreen.toggle()
} label: {
Label("", systemImage: "pencil")
}
}
.padding(10.0)
} label: {
Text(book.title!)
}
}
}.onDelete(perform: deleteBooks)
}
.toolbar {
ToolbarItem(placement: .automatic) {
// here goes blabla
}
}
Text("Bitte zuerst ein Buch auswählen!")
}
.sheet(isPresented: $showingEditScreen) {
// Run EditBookView an send bookId
EditBookView(bookId: selectedBookId).environment(\.managedObjectContext, self.viewContext)
}
}
}
My EditView looks like this
import SwiftUI
struct EditBookView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.dismiss) var dismiss
var bookId: NSManagedObjectID! // This is allways nil!!
var book: Books {
moc.object(with: bookId) as! Books
}
#State private var title = ""
#State private var review = ""
var body: some View {
Form {
Text("Edit Book").font(.title)
Spacer()
Section {
TextField("Buchname", text: $title)
TextEditor(text: $review)
} header: {
Text("Schreibe eine Zusammenfassung")
}
Spacer()
Section {
HStack {
Button("Save") {
// add the book
// here code for update
try? moc.save()
dismiss()
}
Button("Cancel") {
print(bookId) // shows "nil"
dismiss()
}
}
}
Spacer()
}
.onAppear {
self.title = self.book.title ?? ""
self.review = self.book.review ?? ""
}
.padding(10.0)
}
}

First: thanks for all the good hints. In the end, I could solve the problem using
#ObservedObject var aBook: Books
at the beginning of my EditView.
The button itself has the following code
Button {
showingEditScreen.toggle()
} label: {
Label("", systemImage: "pencil")
}.sheet(isPresented: $showingEditScreen) {
EditBookView(aBook: book).environment(\.managedObjectContext, self.viewContext)
}
This way, I can send the whole book object of a single book item to the edit view and I can use it.

Related

Passing data with NavigationSplitView to the second column

I have a macOS app with two columns. The left column is a list that presents the filename and date of the unit (file) that I'm working on. The second column, to the right, should present the content of each file when selected.
I have an array that contains that information and I create a list for the left column that presents each item. I added a detail: with a TextEditor that allows the user to see the data and modify it if necessary. I have been trying to set the #State var text to the contents of currentunit.text but I don't know how to pass that the detail:. If I try to assign it (as in text = x) then I get an error saying that it doesn't conform to View.
I tried then to maybe load it by getting the index of the current selected unit, using the selectedUnitId, and using something like this to get the index:
func getIndex(uuid: UUID) -> Int? {
return data.units.firstIndex(where: {$0.id == uuid})
}
But I get nowhere with a collection of different errors.
Regardless, how do I pass data to the detail: part of the code? I have looked into many examples of NavigationSplitView and they are all very similar, just showing the basic usage and that's it.
Thanks!
Code:
struct Unit: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
let df = DateFormatter()
df.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return df.string(from: date)
}
var changed: Bool = false
}
final class UnitModel: ObservableObject {
#AppStorage("unit") public var units: [Unit] = []
init() {
self.units = self.units.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
func sortList() {
self.units = self.units.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
struct ContentView: View {
#EnvironmentObject private var data: UnitModel
#State var selectedUnitId: UUID?
#State var text: String = ""
var body: some View {
NavigationSplitView {
List(data.units, selection: $selectedUnitId) { currentunit in
VStack(alignment: .leading) {
Text(currentunit.filename)
Text(currentunit.dateText)
}
}
} detail: {
// here: how do I preload $text with the text from the unit?
VStack(alignment: .leading) {
TextEditor(text: $text)
}
}
}
}
I also tried:
struct ContentView: View {
#EnvironmentObject private var data: UnitModel
#State var selectedNoteId: UUID?
var body: some View {
NavigationSplitView {
List(data.units, selection: $selectedNoteId) { currentunit in
NavigationLink{
UnitView(unit: currentunit, text: currentunit.text)
} label: {
VStack(alignment: .leading) {
Text(currentunit.filename)
Text(currentunit.dateText)
}
}
}
} detail: {
Text("Select a unit.")
}
}
}
struct UnitView: View {
#EnvironmentObject private var data: UnitModel
var unit: Unit
#State var text: String
var body: some View {
VStack(alignment: .leading) {
TextEditor(text: $text)
}
}
}
But again, I don't know how to initialize the text variable with the text of the current unit. I only get the initial one selected, and even tho I can see a new unit selected, the text remains the same and doesn't update.
UPDATED if I change the code to use NavigationView then it works as it should, so what's going with the new way that Apple is make us use now? Namely NavigationSplitView and NavigationStack?
Here's the code that work as it should but it's deprecated according to Apple:
NavigationView {
List(data.units, selection: $selectedNoteId) { currentunit in
NavigationLink(
destination: UnitView(unit: currentunit, text: currentunit.text),
label: {
VStack(alignment: .leading) {
Text(currentunit.filename)
Text(currentunit.dateText)
}
}
)
}
Apple's Defining the source of truth using a custom binding
tutorial covers this. Your code would look something like this:
} detail: {
DetailView(unitID: selectedUnitID) // not sure why they used binding
}
struct DetailView: View {
let unitID: Unit.ID
#EnvironmentObject private var store: UnitModel
private var unitBinding: Binding<Unit> {
Binding {
if let id = unitID {
return store.unit(with: id) ?? Unit.emptyUnit()
} else {
return Unit.emptyUnit()
}
} set: { updatedUnit in
store.update(updatedUnit)
}
}
var body: some View {
if store.contains(unitID) {
VStack(alignment: .leading) {
TextEditor(text: unitBinding.text)
}
}
else {
Text("Select Unit")
}
}
}
Note there currently (as of Xcode 14.2) is a known bug with the text cursor when using a TextField in the detail pane. Check by entering text, move cursor to middle and try to enter a character. The bug is the cursor jumps to the end.

Coredata dynamic filters (predicate) in SwiftUI

I found many tutorials pointing out how to create a dynamic filter in Swiftui and Coredata, but none solved my problem, which is to create an entity extension like:
extension Food {
var myFetchRequest: NSFetchRequest<Food> {
let request: NSFetchRequest<Food> = Food.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
request.predicate = NSPredicate(format: "name CONTAINS[c] %#", searchText)
}
}
because obviously I get the following error:
I would like to get the following result:
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjContext
#FetchRequest(fetchRequest: Food.myFetchRequest)
var food: FetchedResults<Food>
#State private var searchText = ""
var body: some View {
NavigationView {
List {
ForEach(food) { food in
NavigationLink(destination: EditFoodView(food: food)) {
HStack {
VStack(alignment: .leading, spacing: 6) {
Text(food.name!)
.bold()
Text("\(Int(food.calories))") + Text(" calories").foregroundColor(.red)
}
Spacer()
Text(calcTimeSince(date: food.date!))
.foregroundColor(.gray)
.italic()
}
}
}
.onDelete(perform: deleteFood)
}
.listStyle(.plain)
}
.navigationTitle("iCalories")
.searchable(text: $searchText)
}
// Deletes food at the current offset
private func deleteFood(offsets: IndexSet) {
withAnimation {
offsets.map { food[$0] }
.forEach(managedObjContext.delete)
// Saves to our database
DataController().save(context: managedObjContext)
}
}
}
How do I have to change extension Food { to make the code work?

SwiftUI for macOS - trigger sheet .onDismiss problem

In a multiplatform app I'm showing a sheet to collect a small amount of user input. On iOS, when the sheet is dismissed, the relevant .onDismiss method is called but not on macOS.
I've read that having the .onDismiss in the List can cause problems so I've attached it to the button itself with no improvement. I've also tried passing the isPresented binding through and toggling that within the sheet itself to dismiss, but again with no success.
I am employing a NavigationView but removing that makes no difference. The following simplified example demonstrates my problem. Any ideas? Should I even be using a sheet for this purpose on macOS?
I just want to make clear that I have no problem closing the sheet. The other questions I found were regarding problems closing the sheet - I can do that fine.
import SwiftUI
#main
struct SheetTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
ListView()
}
}
}
The List view.
struct ListView: View {
#State private var isPresented: Bool = false
var body: some View {
VStack {
Text("Patterns").font(.title)
Button(action: {
isPresented = true
}, label: {
Text("Add")
})
.sheet(isPresented: $isPresented, onDismiss: {
doSomethingAfter()
}) {
TestSheetView()
}
List {
Text("Bingo")
Text("Bongo")
Text("Banjo")
}
.onAppear(perform: {
doSomethingBefore()
})
}
}
func doSomethingBefore() {
print("Johnny")
}
func doSomethingAfter() {
print("Cash")
}
}
This is the sheet view.
struct TestSheetView: View {
#Environment(\.presentationMode) var presentationMode
#State private var name = ""
var body: some View {
Form {
TextField("Enter name", text: $name)
.padding()
HStack {
Spacer()
Button("Save") {
presentationMode.wrappedValue.dismiss()
}
Spacer()
}
}
.frame(minWidth: 300, minHeight: 300)
.navigationTitle("Jedward")
}
}
Bad issue.. you are right. OnDismiss is not called. Here is a workaround with Proxybinding
var body: some View {
VStack {
Text("Patterns").font(.title)
Button(action: {
isPresented = true
}, label: {
Text("Add")
})
List {
Text("Bingo")
Text("Bongo")
Text("Banjo")
}
.onAppear(perform: {
doSomethingBefore()
})
}
.sheet(isPresented: Binding<Bool>(
get: {
isPresented
}, set: {
isPresented = $0
if !$0 {
doSomethingAfter()
}
})) {
TestSheetView()
}
}

SwiftUI on macOS: list with detail view and multiple selection

TL;DR:
I cannot have a list with a detail view and multiple selections on macOS.
In more detail:
For demonstration purposes of my issue, I made a small example project. The UI looks as follows:
This is the "app" when launched, with a list on top and a detail representation below. Because I am using the List's initialiser init(_:selection:rowContent:), where selection is of type Binding<SelectionValue?>? according to Apple's documentation, I get selecting items with the keyboard arrow keys for free.
Here's the complete code:
import SwiftUI
#main
struct UseCurorsInLisstApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(ViewModel())
}
}
}
class ViewModel: ObservableObject {
#Published var items = [Item(), Item(), Item(), Item(), Item()]
#Published var selectedItem: Item? = nil
}
struct Item: Identifiable, Hashable {
let id = UUID()
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
VStack {
List(vm.items, id: \.self, selection: $vm.selectedItem) { item in
VStack {
Text("Item \(item.id.uuidString)")
Divider()
}
}
Divider()
Group {
if let item = vm.selectedItem {
Text("Detail item \(item.id.uuidString)")
} else {
Text("No selection…")
}
}
.frame(minHeight: 200.0, maxHeight: .infinity)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Now, having had success with this so far, I figured being able to select more than one row would be useful, so I took a closer look into List(_:selection:rowContent:), where selection is of type Binding<Set<SelectionValue>>?. To be able to have a detail view, I just made a few minor changes to
the ViewModel:
class ViewModel: ObservableObject {
#Published var items = [Item(), Item(), Item(), Item(), Item()]
#Published var selectedItem: Item? = nil
#Published var selectedItems: Set<Item>? = nil {
didSet {
if selectedItems?.count == 1, let item = selectedItems?.first {
selectedItem = item
}
}
}
}
and the ContentView:
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
VStack {
List(vm.items, id: \.self, selection: $vm.selectedItems) { item in
VStack {
Text("Item \(item.id.uuidString)")
Divider()
}
}
Divider()
Group {
if vm.selectedItems?.count == 1, let item = vm.selectedItems?.first {
Text("Detail item \(item.id.uuidString)")
} else {
Text("No or multiple selection…")
}
}
.frame(minHeight: 200.0, maxHeight: .infinity)
}
}
}
The problem now is that I cannot select an item of the row any more, neither by clicking, nor by arrow keys. Is this a limitation I am running into or am I "holding it wrong"?
Use the button and insert it into the set. Keyboard selection also works with shift + (up/down arrow)
class ViewModel: ObservableObject {
#Published var items = [Item(), Item(), Item(), Item(), Item()]
#Published var selectedItem: Item? = nil
#Published var selectedItems: Set<Item> = []
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
VStack {
List(vm.items, id: \.self, selection: $vm.selectedItems) { item in
Button {
vm.selectedItem = item
vm.selectedItems.insert(item)
} label: {
VStack {
Text("Item \(item.id.uuidString)")
Divider()
}
}
.buttonStyle(PlainButtonStyle())
}
Divider()
Group {
if let item = vm.selectedItem {
Text("Detail item \(item.id.uuidString)")
} else {
Text("No or multiple selection…")
}
}
.frame(minHeight: 200.0, maxHeight: .infinity)
}
}
}
Add remove:
Button {
vm.selectedItem = item
if vm.selectedItems.contains(item) {
vm.selectedItems.remove(item)
} else {
vm.selectedItems.insert(item)
}
}
Edit
In simple need to give a blank default value to set. because in nil it will never append to set need initialization.
#Published var selectedItems: Set<Item> = [] {
Actually my error was pretty dumb – making the selectedItems-set optional prevents the list from working correctly. Shoutout to #Raja Kishan, who pushed me into the right direction with his proposal.
Here's the complete working code:
import SwiftUI
#main
struct UseCurorsInLisstApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(ViewModel())
}
}
}
class ViewModel: ObservableObject {
#Published var items = [Item(), Item(), Item(), Item(), Item()]
#Published var selectedItems = Set<Item>()
}
struct Item: Identifiable, Hashable {
let id = UUID()
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
VStack {
List(vm.items, id: \.self, selection: $vm.selectedItems) { item in
VStack {
Text("Item \(item.id.uuidString)")
Divider()
}
}
Divider()
Group {
if vm.selectedItems.count == 1, let item = vm.selectedItems.first {
Text("Detail item \(item.id.uuidString)")
} else {
Text("No or multiple selection…")
}
}
.frame(minHeight: 200.0, maxHeight: .infinity)
}
}
}

Why cant I see my for Each loop in the preview view in XCode

I am making a to do list to learn about Core data. I noticed when I add a ForEach loop to read my core data entries, it stops loading the view and the app crashes. If I replace the ForEach loop with a Text("Hi"), it all works. However the app runs perfectly in simulator.
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: ToDoItem.getAllToDo()) var toDoItems:FetchedResults<ToDoItem>
#State private var newTodoItem = ""
#State private var selection = 0
var body: some View {
NavigationView {
List {
Section (header: Text("Whats... next")) {
HStack{
TextField("New Item", text: self.$newTodoItem)
Button(action: {
let toDoItem = ToDoItem(context: self.managedObjectContext)
toDoItem.title = self.newTodoItem
toDoItem.createdAt = Date()
do {
try self.managedObjectContext.save()
}catch {
print(error)
}
self.newTodoItem = ""
}){
Image(systemName: "plus.circle.fill")
.foregroundColor(.green)
.imageScale(.large)
}
}
}.font(.headline)
Section(header: Text("To Do's")){
ForEach(self.toDoItems) {todoItem in
HStack{
VStack(alignment: .leading){
Text(todoItem.title!)
.font(.headline)
Text("\(todoItem.createdAt!)")
.font(.caption)
}
}
}.onDelete {indexSet in
let deleteItem = self.toDoItems[indexSet.first!]
self.managedObjectContext.delete(deleteItem)
do {
try self.managedObjectContext.save()
}catch {
print(error)
}
}
}
}
.navigationBarTitle(Text("My List"))
.navigationBarItems(trailing: EditButton())
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any idea what the issue is?

Resources