Can't Pass Data Correctly to Modal Presentation Using Foreach and Coredata in Swiftui

Can’t pass data correctly to modal presentation using ForEach and CoreData in SwiftUI

There should be only one sheet in view stack, so just move it out of ForEach, like below

struct CarScrollView: View {

@Environment(\.managedObjectContext) var moc
@FetchRequest(entity: Cars.entity(), sortDescriptors: []) var cars: FetchedResults<Cars>

@State private var selectedCar: Car? = nil

var body: some View {
VStack {
ScrollView (.vertical, showsIndicators: false) {
ForEach (cars, id: \.self) { car in

Text("\(car.text!)")
.onTapGesture {
self.selectedCar = car
}
}
}
}
.sheet(item: self.$selectedCar) { car in
CarDetail(id: car.id, text: car.text)
}

}
}

SwiftUI using ForEach and onTapGesture to update selected element causing crash

Your problem is likely due to the view diffing algorithm having a different outcome if you have the text value present, since that would be a difference in the root view. Since you have two pieces of state that represent the sheet, and you are force unwrapping one of them, you're leaving yourself open to danger.

You should get rid of showingSheet, then use the item binding of sheet instead. Make Cards conform to Identifiable, then rewrite the sheet modifier as:

.sheet(item: $selectedCard) { SpecificCardView(card: $0) } 

This will also reset your selected card to nil when the sheet is dismissed.

Selecting an existing item on an array or passing a new one to a .sheet

From this post, you can do like this in your case:

struct SheetForNewAndEdit: View {
@EnvironmentObject private var data: DataModel
@State var searchText: String = ""
// The selected row
@State var selectedNote: NoteItem? = nil
@State private var sheetNewNote = false
// for test :
@State private var filteredNotes: [NoteItem] = [
NoteItem(id: UUID(), text: "111"),

NoteItem(id: UUID(), text: "222")];

var body: some View {
NavigationView {
List(filteredNotes) { note in
VStack(alignment: .leading) {
//....
// not relevant code

Text(note.text)
}
.swipeActions(allowsFullSwipe: false) {
Button(action: {
// the action select the note to display
selectedNote = note
} ) {
Label("Edit", systemImage: "pencil")
}
}
}
// sheet is displayed depending on selected note
.sheet(item: $selectedNote, content: {
note in
SheetView(note: note)
})
// moved tool bar one level (inside navigation view)
.toolbar {
// Toolbar item to have toolbar
ToolbarItemGroup(placement: .navigationBarTrailing) {
ZStack {
Button(action: {
// change bool value
self.sheetNewNote.toggle()
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
.sheet(isPresented: $sheetNewNote) {
let Note = NoteItem(id: UUID(), text: "New Note", date: Date(), tags: [])
SheetView(note: Note)
}
}
}
}

Note : SheetView does not need any more a boolean, but you can add one if you orefer

How to use a picker on CoreData relationships in SwiftUI

OK. Huge vote of thanks to Lorem for getting me to the answer. Thanks too for Roma, but it does turn out that his solution, whilst it worked to resolve one of my key problems, does introduce inefficiencies - and didn't resolve the second one.

If others are hitting the same issue I'll leave the Github repo up, but the crux of it all was that @State shouldn't be used when you're sharing CoreData objects around. @ObservedObject is the way to go here.

So the resolution to the problems I encountered were:

  1. Use @ObservedObject instead of @State for passing around the CoreData objects
  2. Make sure that the picker has a tag defined. The documentation I head read implied that this gets generated automatically if you use ".self" as the id for the objects in ForEach, but it seems this is not always reliable. so adding ".tag(element as Element?)" to my picker helped here.
    Note: It needed to be an optional type because CoreData makes all the attribute types optional.

Those two alone fixed the problems.

The revised "LicenceView" struct is here, but the whole solution is in the repo.

Cheers!

struct LicenceView : View {
@Environment(\.managedObjectContext) private var viewContext
@ObservedObject var licence: Licence
@Binding var showModal: Bool

@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Element.desc, ascending: true)],
animation: .default)
private var elements: FetchedResults<Element>

func save() {
try! viewContext.save()
showModal = false
}

var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $licence.licenced, label: Text("Element")) {
ForEach(elements, id: \.self) { element in
Text("\(element.desc!)")
.tag(element as Element?)
}
}
Text("Selected: \(licence.licenced!.desc!)")
Button(action: {save()}) {
Text("Save")
}
}

}
}

Sheet contents won't update depending on parameters

You already did it the right way with @ObservedObject var team: Team

Simply change var event: MatchEvent to @ObservedObject var event: MatchEvent.

The @ObservedObject Property Wrapper comes from Combine and is "looking for Updates and updating the View once an Update was found".

Editing a specific core data object in modal view

The fetchedEvent is not available yet at View.init call moment, so instead use it in .didAppear, like below

// ... remove that init at all

var body: some View {
Text("Any internal view here")
.onAppear {
// assigned fetched event data, here it is available
self.selectedDate = self.fetchedEvent.first?.eventDate ?? Date()
}
}

Core Data - reflect parent context changed to child context without saving child

You have a few options here

  1. Configure the sheet so that it closes when the item is deleted.
  2. enable automaticallyMergesChangesFromParent on the child context so that changes from the parent are merged in. Note this can also be done manually by listening for the save notification and searching for only the changes of interest.
  3. Edit your model to enforce the relation and handle the error on save.

View is not rerendered in Nested ForEach loop


TL;DR

Your ForEach needs id: \.self added after your range.

Explanation

ForEach has several initializers. You are using

init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)

where data must be a constant.

If your range may change (e.g. you are adding or removing items from an array, which will change the upper bound), then you need to use

init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)

You supply a keypath to the id parameter, which uniquely identifies each element that ForEach loops over. In the case of a Range<Int>, the element you are looping over is an Int specifying the array index, which is unique. Therefore you can simply use the \.self keypath to have the ForEach identify each index element by its own value.

Here is what it looks like in practice:

struct ContentView: View {
@State var array = [1, 2, 3]

var body: some View {
VStack {
Button("Add") {
self.array.append(self.array.last! + 1)
}

// this is the key part v--------v
ForEach(0..<array.count, id: \.self) { index in
Text("\(index): \(self.array[index])")
//Note: If you want more than one views here, you need a VStack or some container, or will throw errors
}
}
}
}

If you run that, you'll see that as you press the button to add items to the array, they will appear in the VStack automatically. If you remove "id: \.self", you'll see your original error:

`ForEach(_:content:)` should only be used for *constant* data. 
Instead conform data to `Identifiable` or use `ForEach(_:id:content:)`
and provide an explicit `id`!"

SwiftUI: Cannot dismiss sheet after creating CoreData object

Your .sheet is placed on the ToolbarItem. Just change it on the NavigationView and it works.

Here is the body of the ContentView

var body: some View {
NavigationView {
List {
//Text("static text")

ForEach(items, id:\.self) { item in
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
.onDelete(perform: deleteItems)
}
.navigationTitle("List of items")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
showingSheet = true
}) {
Text("Open sheet")
}
}
}
.sheet(isPresented: $showingSheet) {
MySheetView(isPresented: $showingSheet)
.environment(\.managedObjectContext, viewContext)
}

}
}


Related Topics



Leave a reply



Submit