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:
- Use @ObservedObject instead of @State for passing around the CoreData objects
- 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
- Configure the sheet so that it closes when the item is deleted.
- 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.
- 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
Creating a Navigationcontroller Programmatically (Swift)
Uitapgesturerecognizer Tap on Self.View But Ignore Subviews
Adding Google Maps as Subview Crashes iOS App with Exc_Bad
How to Open Fb and Instagram App by Tapping on Button in Swift
How to Authenticate the Gklocalplayer on My 'Third Party Server'
Error When Trying to Obtain a Certificate: the Specified Item Could Not Be Found in the Keychain
Change String Color with Nsattributedstring
How to Apply Multiple Transforms in Swift
Auto Layout Uiscrollview with Subviews with Dynamic Heights
iPhone Is Not Available. Please Reconnect the Device
How to Disable Uitextfield Editing But Still Accept Touch
Auto-Layout: What Creates Constraints Named Uiview-Encapsulated-Layout-Width & Height
Waiting for Multiple Asynchronous Download Tasks
Centering Subview's X in Autolayout Throws "Not Prepared for the Constraint"
What Is Kcferrordomaincfnetwork Code=303
How to Get Only Images in the Camera Roll Using Photos Framework
Simple Way to Change the Position of Uiview
How Does Square's Cardcase App Automatically Populate the User's Details from the Address Book