Problem saving data, using CoreData with SwiftUI - ios

I just started using CoreData with SwiftUI.
After following this series of tutorial.
And I am facing a situation where I cannot save data as I expect. Here is the relevant code:
struct MyView: View {
......
#Environment(\.managedObjectContext) var managedObjectContext
func setData() {
print(#function)
.....
let myData = SomeEntity(context: self.managedObjectContext)
myData.name = "myName"
myData......
do {
try self.managedObjectContext.save()
} catch let error {
// Handle the Core Data error.
print("Can't save as expected!!!!")
print("Error : \(error)")
}
}
.....
var body: some View {
........
}
}
When this code executes I get this error:
Can't save as expected!!!!
Error : nilError
Can somebody tell me what I need to check?
I am all the more puzzled, that I have another part in my app (apparently similar), in a view one level above, where the saving is perfectly working.
In case this may be useful, the view is presented with code like this:
}).sheet(isPresented: $showingFlag) {
MyView(.....)
}
In the presenting view, data saving is working.

Sheets and alerts, etc are modal views. Modal views are normal views, but they are not part of the interface's view hierarchy and are presented on top of the rest of the views on the screen. This means they do not have automatic access to the same #Environment variables that were injected into the interface's view hierarchy (usually in the SceneDelegate or the newer SwiftUI App life-cycle).
To access that same managedObjectContext variable inside the modal View you can pass it along using standard dependency injection.
}).sheet(isPresented: $showingFlag) {
MyView(context: managedObjectContext)
}
struct MyView: View {
......
var context: NSManagedObectContext!
func setData() {
print(#function)

Related

Changes to a struct not being published in SwiftUI

I'm loading data from an API, and expecting my app to show the data once it's loaded.
In my View Model file, here's the code:
It calls a WeatherService to get the data, and populates the weather property. Weather is a struct in this case.
class WeatherViewModel: ObservableObject {
let webService = WeatherService.shared
#Published var weather:Weather?
init() {
}
func getWeather() {
webService.getWeather { weather in
if let weather = weather {
self.weather = weather
}
}
}
}
In my SwiftUI view, here's the code:
I instantiate an instance of the View Model as an ObservedObject
In the inAppear, I call the method in the view model to get the data
The first time the screen launches (using a tab bar), I see "Loading weather..." and it never goes away
If I navigate to a different tab and back, I see the weather. I can't tell if this is data from the old API call, or from the new one.
struct WeatherView: View {
#ObservedObject var weatherViewModel = WeatherViewModel()
#State var areDetailsHidden = true
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if(weatherViewModel.weather == nil) {
Text("Loading weather...")
} else {
Text("Display the weather here")
}
}
.onAppear{
self.weatherViewModel.getWeather()
}
}
}
The weird thing is, if I remove the getWeather() from the onAppear and add it to the init() of the View Model, it works (although for some reason getWeather() gets called twice...). However, I want the weather info to be refreshed every time the screen is loaded.
This is caused by the:
#ObservedObject var weatherViewModel = WeatherViewModel()
being owned by the WeatherView itself.
So what happens is the weather view model changes which forces a re-render of the view which creates a new copy of the weather view model, which changes forces a re-render...
So you end up with an endless loop.
To fix it you need to move the weather view model out of the view itself so either use an #Binding and pass it in or an #EnvironmentObject and access it that way.

SwiftUI custom View's ViewBuilder doesn't re-render/update on subclassed ObservedObject update

This one I've been researching for a few days, scouring the Swift & SwiftUI docs, SO, forums, etc. and can't seem to find an answer.
Here is the problem;
I have a SwiftUI custom View that does some state determination on a custom API request class to a remote resource. The View handles showing loading states and failure states, along with its body contents being passed through via ViewBuilder so that if the state from the API is successful and the resource data is loaded, it will show the contents of the page.
The issue is, the ViewBuilder contents does not re-render when the subclassed ObservedObject updates. The Object updates in reaction to the UI (when buttons are pressed, etc.) but the UI never re-renders/updates to reflect the change within the subclassed ObservedObject, for example the ForEach behind an array within the subclassed ObservedObject does not refresh when the array contents change. If I move it out of the custom View, the ForEach works as intended.
I can confirm the code compiles and runs. Observers and debugPrint()'s throughout show that the ApiObject is updating state correctly and the View reflects the ApiState change absolutely fine. It's just the Content of the ViewBuilder. In which I assume is because the ViewBuilder will only ever be called once.
EDIT: The above paragraph should have been the hint, the ApiState updates correctly, but after putting extensive logging into the application, the UI was not listening to the publishing of the subclassed ObservedObject. The properties were changing and the state was too, but the UI wasn't being reactive to it.
Also, the next sentence turned out to be false, I tested again in a VStack and the component still didn't re-render, meaning I was looking in the wrong place!
If this is the case, how does VStack and other such elements get around this?
Or is it because my ApiObjectView is being re-rendered on the state change, in which causes the child view to 'reset'? Although in this circumstance I'd expect it to then take on the new data and work as expected anyway, its just never re-rendering.
The problematic code is in the CustomDataList.swift and ApiObjectView.swift below. I've left comments to point in the right direction.
Here is the example code;
// ApiState.swift
// Stores the API state for where the request and data parse is currently at.
// This drives the ApiObjectView state UI.
import Foundation
enum ApiState: String
{
case isIdle
case isFetchingData
case hasFailedToFetchData
case isLoadingData
case hasFailedToLoadData
case hasUsableData
}
// ApiObject.swift
// A base class that the Controllers for the app extend from.
// These classes can make data requests to the remote resource API over the
// network to feed their internal data stores.
class ApiObject: ObservableObject
{
#Published var apiState: ApiState = .isIdle
let networkRequest: NetworkRequest = NetworkRequest(baseUrl: "https://api.example.com/api")
public func apiGetJson<T: Codable>(to: String, decodeAs: T.Type, onDecode: #escaping (_ unwrappedJson: T) -> Void) -> Void
{
self.apiState = .isFetchingData
self.networkRequest.send(
to: to,
onComplete: {
self.apiState = .isLoadingData
let json = self.networkRequest.decodeJsonFromResponse(decodeAs: decodeAs)
guard let unwrappedJson = json else {
self.apiState = .hasFailedToLoadData
return
}
onDecode(unwrappedJson)
self.apiState = .hasUsableData
},
onFail: {
self.apiState = .hasFailedToFetchData
}
)
}
}
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
// Subclassed from the ApiObject, inheriting ObservableObject
import Foundation
import Combine
class CustomDataController: ApiObject
{
#Published public var customData: [CustomDataStruct] = []
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
This is the View that has the problem with re-rendering its ForEach on the ObservedObject change to its bound array property.
// CustomDataList.swift
// This is the SwiftUI View that drives the content to the user as a list
// that displays the CustomDataController.customData.
// The ForEach in this View
import SwiftUI
struct CustomDataList: View
{
#ObservedObject var customDataController: CustomDataController = CustomDataController()
var body: some View
{
ApiObjectView(
apiObject: self.customDataController,
onQuit: {}
) {
List
{
Section(header: Text("Custom Data").padding(.top, 40))
{
ForEach(self.customDataController.customData, id: \.self, content: { customData in
// This is the example that doesn't re-render when the
// customDataController updates its data. I have
// verified via printing at watching properties
// that the object is updating and pushing the
// change.
// The ObservableObject updates the array, but this ForEach
// is not run again when the data is changed.
// In the production code, there are buttons in here that
// change the array data held within customDataController.customData.
// When tapped, they update the array and the ForEach, when placed
// in the body directly does reflect the change when
// customDataController.customData updates.
// However, when inside the ApiObjectView, as by this example,
// it does not.
Text(customData.textProperty)
})
}
}
.listStyle(GroupedListStyle())
}
.navigationBarTitle(Text("Learn"))
.onAppear() {
self.customDataController.fetch()
}
}
}
struct CustomDataList_Previews: PreviewProvider
{
static var previews: some View
{
CustomDataList()
}
}
This is the custom View in question that doesn't re-render its Content.
// ApiObjectView
// This is the containing View that is designed to assist in the UI rendering of ApiObjects
// by handling the state automatically and only showing the ViewBuilder contents when
// the state is such that the data is loaded and ready, in a non errornous, ready state.
// The ViewBuilder contents loads fine when the view is rendered or the state changes,
// but the Content is never re-rendered if it changes.
// The state renders fine and is reactive to the object, the apiObjectContent
// however, is not.
import SwiftUI
struct ApiObjectView<Content: View>: View {
#ObservedObject var apiObject: ApiObject
let onQuit: () -> Void
let apiObjectContent: () -> Content
#inlinable public init(apiObject: ApiObject, onQuit: #escaping () -> Void, #ViewBuilder content: #escaping () -> Content) {
self.apiObject = apiObject
self.onQuit = onQuit
self.apiObjectContent = content
}
func determineViewBody() -> AnyView
{
switch (self.apiObject.apiState) {
case .isIdle:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .isFetchingData:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .isLoadingData:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .hasFailedToFetchData:
return AnyView(
VStack
{
Text("Failed to load data!")
.padding(.bottom)
QuitButton(action: self.onQuit)
}
)
case .hasFailedToLoadData:
return AnyView(
VStack
{
Text("Failed to load data!")
.padding(.bottom)
QuitButton(action: self.onQuit)
}
)
case .hasUsableData:
return AnyView(
VStack
{
self.apiObjectContent()
}
)
}
}
var body: some View
{
self.determineViewBody()
}
}
struct ApiObjectView_Previews: PreviewProvider {
static var previews: some View {
ApiObjectView(
apiObject: ApiObject(),
onQuit: {
print("I quit.")
}
) {
EmptyView()
}
}
}
Now, all the above code works absolutely fine, if the ApiObjectView isn't used and the contents placed in the View directly.
But, that is horrendous for code reuse and architecture, this way its nice and neat, but doesn't work.
Is there any other way to approach this, e.g. via a ViewModifier or a View extension?
Any help on this would be really appreciated.
As I said, I can't seem to find anyone with this problem or any resource online that can point me in the right direction to solve this problem, or what might be causing it, such as outlined in documentation for ViewBuilder.
EDIT: To throw something interesting in, I've since added a countdown timer to CustomDataList, which updates a label every 1 second. IF the text is updated by that timer object, the view is re-rendered, but ONLY when the text on the label displaying the countdown time is updated.
Figured it out after pulling my hair out for a week, its an undocumented issue with subclassing an ObservableObject, as seen in this SO answer.
This is particularily annoying as Xcode obviously prompts you to remove the class as the parent class provides that inheritence to ObservableObject, so in my mind all was well.
The fix is, within the subclassed class to manually fire the generic state change self.objectWillChange.send() via the willSet listener on the #Published variable in question, or any you require.
In the examples I provided, the base class ApiObject in the question remains the same.
Although, the CustomDataController needs to be modified as follows:
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
import Foundation
import Combine
class CustomDataController: ApiObject
{
#Published public var customData: [CustomDataStruct] = [] {
willSet {
// This is the generic state change fire that needs to be added.
self.objectWillChange.send()
}
}
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
As soon as I added that manual publishing, the issue is resolved.
An important note from the linked answer: Do not redeclare objectWillChange on the subclass, as that will again cause the state not to update properly. E.g. declaring the default
let objectWillChange = PassthroughSubject<Void, Never>()
on the subclass will break the state updating again, this needs to remain on the parent class that extends from ObservableObject directly, either my manual or automatic default definition (typed out, or not and left as inherited declaration).
Although you can still define as many custom PassthroughSubject declarations as you require without issue on the subclass, e.g.
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
import Foundation
import Combine
class CustomDataController: ApiObject
{
var customDataWillUpdate = PassthroughSubject<[CustomDataStruct], Never>()
#Published public var customData: [CustomDataStruct] = [] {
willSet {
// Custom state change handler.
self.customDataWillUpdate.send(newValue)
// This is the generic state change fire that needs to be added.
self.objectWillChange.send()
}
}
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
As long as
The self.objectWillChange.send() remains on the #Published properties you need on the subclass
The default PassthroughSubject declaration is not re-declared on the subclass
It will work and propagate the state change correctly.

SwiftUI holding reference to deleted core data object causing crash

Im finding it impossible to use core data with SwiftUI, because as I pass a core data to a view observed object variable, the navigation link view will hold a reference to the object even after the view has disappeared, so as soon as I delete the object from context the app crashes, with no error messages.
I have confirmed this by wrapping the core data object variable into a view model as an optional, then set the object to nil right after the context delete action and the app works fine, but this is not a solution because I need the core data object to bind to the swift ui views and be the source of truth. How is this suppose to work? I seriously cannot make anything remotely complex with SwiftUI it seems.
I have tried assigning the passed in core data object to a optional #State, but this does not work. I cannot use #Binding because it's a fetched object. And I cannot use a variable, as swiftui controls require bindings. It only makes sense to use a #ObservedObject, but this cannot be an optional, which means when the object assigned to it gets deleted, the app crashes, because i cannot set it to nil.
Here is the core data object, which is an observable object by default:
class Entry: NSManagedObject, Identifiable {
#NSManaged public var date: Date
}
Here is a view that passes a core data entry object to another view.
struct JournalView: View {
#Environment(\.managedObjectContext) private var context
#FetchRequest(
entity: Entry.entity(),
sortDescriptors: [],
predicate: nil,
animation: .default
) var entries: FetchedResults<Entry>
var body: some View {
NavigationView {
List {
ForEach(entries.indices) { index in
NavigationLink(destination: EntryView(entry: self.entries[index])) {
Text("Entry")
}
}.onDelete { indexSet in
for index in indexSet {
self.context.delete(self.entries[index])
}
}
}
}
}
}
Now here is the view that accesses all the attributes from the core data entry object that was passed in. Once, I delete this entry, from any view by the way, it is still referenced here and causes the app to crash immediately. I believe this also has something to do with the Navigation Link initializing all destination view before they are even accessed. Which makes no sense why it would do that. Is this a bug, or is there a better way to achieve this?
I have even tried doing the delete onDisappear with no success. Even if I do the delete from the JournalView, it will still crash as the NavigationLink is still referencing the object. Interesting it will not crash if deleting a NavigationLink that has not yet been clicked on.
struct EntryView: View {
#Environment(\.managedObjectContext) private var context
#Environment(\.presentationMode) private var presentationMode
#ObservedObject var entry: Entry
var body: some View {
Form {
DatePicker(selection: $entry.date) {
Text("Date")
}
Button(action: {
self.context.delete(self.entry)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Delete")
}
}
}
}
UPDATE
The crash is taking me to the first use of entry in the EntryView and reads Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).. thats the only message thrown.
The only work around I can think of is to add a property to the core data object "isDeleted" and set it to true instead of trying to delete from context. Then when the app is quit, or on launch, I can clean and delete all entries that isDeleted? Not ideal, and would prefer to figure out what it wrong here, as it appears I'm not doing anything different then the MasterDetailApp sample, which seems to work.
I basically had the same issue. It seems that SwiftUI loads every view immediately, so the view has been loaded with the Properties of the existing CoreData Object. If you delete it within the View where some data is accessed via #ObservedObject, it will crash.
My Workaround:
The Delete Action - postponed, but ended via Notification Center
Button(action: {
//Send Message that the Item should be deleted
NotificationCenter.default.post(name: .didSelectDeleteDItem, object: nil)
//Navigate to a view where the CoreDate Object isn't made available via a property wrapper
self.presentationMode.wrappedValue.dismiss()
})
{Text("Delete Item")}
You need to define a Notification.name, like:
extension Notification.Name {
static var didSelectDeleteItem: Notification.Name {
return Notification.Name("Delete Item")
}
}
On the appropriate View, lookout for the Delete Message
// Receive Message that the Disease should be deleted
.onReceive(NotificationCenter.default.publisher(for: .didSelectDeleteDisease)) {_ in
//1: Dismiss the View (IF It also contains Data from the Item!!)
self.presentationMode.wrappedValue.dismiss()
//2: Start deleting Disease - AFTER view has been dismissed
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(1)) {self.dataStorage.deleteDisease(id: self.diseaseDetail.id)}
}
Be safe on your Views where some CoreData elements are accessed - Check for isFault!
VStack{
//Important: Only display text if the disease item is available!!!!
if !diseaseDetail.isFault {
Text (self.diseaseDetail.text)
} else { EmptyView() }
}
A little bit hacky, but this works for me.
I encountered the same issue and did not really find a solution to the root problem. But now I "protect" the view that uses the referenced data like this:
var body: some View {
if (clip.isFault) {
return AnyView(EmptyView())
} else {
return AnyView(actualClipView)
}
}
var actualClipView: some View {
// …the actual view code accessing various fields in clip
}
That also feelds hacky, but works fine for now. It's less complex than using a notification to "defer" deletion, but still thanks to sTOOs answer for the hint with .isFault!
I have had the same issue for a while, the solution for me was pretty simple:
In the View where the #ObservedObject is stored I simply put this !managedObject.isFault.
I experienced this class only with ManagedObjects with a date property, I don't know if this is the only circumstance the crash verifies.
import SwiftUI
struct Cell: View {
#ObservedObject var managedObject: MyNSManagedObject
var body: some View {
if !managedObject.isFault {
Text("\(managedObject.formattedDate)")
} else {
ProgressView()
}
}
}
After some research online, it's clear to me that this crash can be caused by many things related to optionals. For me, I realized that declaring a non-optional Core Data attribute as an optional in the NSManagedObject subclass was causing the issue.
Specifically, I have a UUID attribute id in Core Data that cannot have a default value, but is not optional. In my subclass, I declared #NSManaged public var id: UUID. Changing this to #NSManaged public var id: UUID? fixed the problem immediately.
I had the same issue recently. Adding an entity property to the view fixed it.
ForEach(entities, id: \.self) { entity in
Button(action: {
}) {
MyCell(entity: entity)
}
}
To
ForEach(entities, id: \.self) { entity in
Button(action: {
}) {
MyCell(entity: entity, property: entity.property)
}
}
I suspect that the nullable Core Data entity is the cause of the issue, where as adding a non-nil property as a var (e.g, var property: String) fixed it
I have tried all previous solutions, none worked for me.
This one, worked.
I had my list like this:
List {
ForEach(filteredItems, id: \.self) { item in
ListItem(item:item)
}
.onDelete(perform: deleteItems)
private func deleteItems(offsets: IndexSet) {
//deleting items
This was crashing.
I modified the code to this one
List {
ForEach(filteredItems, id: \.self) { item in
ListItem(item:item)
}
.onDelete { offsets in
// delete objects
}
This works fine without crashing.
For heaven's sake, Apple!
A view modifier for this (based on conditional view modifiers):
import SwiftUI
import CoreData
extension View {
#ViewBuilder
func `if`<Transform: View>(
_ condition: Bool,
transform: (Self) -> Transform
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
extension View {
func hidingFaults(_ object: NSManagedObject) -> some View {
self.if(object.isFault) { _ in EmptyView() }
}
}
Having said that, it's worth checking you're performing CoreData operations asynchronously on the main thread, doing it synchronously can be a source of grief (sometimes, but not always).
Apple says this (and it works perfectly) :
The behavior you've reported is the result of a system bug, and should
be fixed in a future release. As a workaround, you can prevent the
race condition by wrapping your deletion logic in
NSManagedObjectContext.perform:
private func deleteItems(offsets: IndexSet) {
withAnimation {
viewContext.perform {
offsets.map { molts[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
viewContext.rollback()
userMessage = "\(error): \(error.localizedDescription)"
displayMessage.toggle()
}
}
}
You can find the full thread here https://developer.apple.com/forums/thread/668299
For me, I got this because of a force-unwrapped binding.
I used a Binding($item.someProperty)! like this: TextField("Description", text: Binding($item.someProperty)!).
This was because item is a Core Data class and hence someProperty is a String? instead of a String. Binding(*)! was a solution proposed in https://stackoverflow.com/a/59004832.
I changed the implementation to use a null coalescing operator for bindings as proposed in https://stackoverflow.com/a/61002589, now it doesn't crash anymore.
Wrap your deletion logic in a withAnimation block to prevent a crash after deleting a Core Data object. No need for isFault, isDeleted, or deferring execution in other complicated ways.
withAnimation {
context.delete(object)
do try catch etc...
}

Saving core data entity in popover in SwiftUI throws nilError without passing .environment to SubView again

Playin' with SwiftUI and Core Data brought me into a curious problem. So the situation is the following:
I have a main view "AppView" and a sub view named "SubView". The SubView view will be opened from the AppView view if I click the plus button in the NavigationTitleBar as popover or sheet.
#Environment(\.managedObjectContext) var managedObjectContext
#State private var modal: Bool = false
...
Button(action: {
self.modal.toggle()
}) {
Image(systemName: "plus")
}.popover(isPresented: self.$modal){
SubView()
}
The SubView view has a little form with two TextField objects to add a forename and a surname. The inputs of this two objects are handled by two separate #State properties. The third object in this form is simple button, which should save a the fore- and surname to an attached Customer Entity for CoreData.
...
#Environment(\.managedObjectContext) var managedObjectContext
...
Button(action: {
let customerItem = Customer(context: self.managedObjectContext)
customerItem.foreName = self.forename
customerItem.surname = self.surname
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}) {
Text("Speichern")
}
If I try to save the Customer entity this way, I get the error: "nilError", specially: "Unresolved error Error Domain=Foundation._GenericObjCError Code=0 "(null)", [:]" from NSError.
But after figuring out, that when I add .environment(\.managedObjectContext, context) to the SubView() call like so SubView().environment(\.managedObjectContext, context) it's works like a charm.
Does anyone know, why I need to pass the managedObjectContext a second time? I thought, that I just need to pass the managedObjectContext one time to use it in the whole view hierarchy, like in the SceneDelegate.swift:
// Get the managed object context from the shared persistent container.
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
// Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
// Add `#Environment(\.managedObjectContext)` in the views that will need the context.
let contentView = AppView().environment(\.managedObjectContext, context)
Is it because calling the SubView() this way, the view is not part of the view hierarchy? I don't understand it...
WOW THIS DROVE ME NUTS! Especially because the errors tells you absolutely no information as to how to fix.
Here is the fix until the bug in Xcode is resolved:
.navigationBarItems(trailing:
Button(action: {
self.add = true
}, label: {
Text("Add Todo List")
}).sheet(isPresented: $add, content: {
AddTodoListView().environment(\.managedObjectContext, managedObjectContext)
})
)
Just add .environment(\.managedObjectContext, managedObjectContext) to your secondary view (a Modal, in this example).

How to create an instance of an object in SwiftUI without duplication?

This is the next part of that question.
I've got the follow code.
The initial view of the app:
struct InitialView : View {
var body: some View {
Group {
PresentationButton(destination: ObjectsListView()) {
Text("Show ListView")
}
PresentationButton(destination: AnotherObjectsListView()) {
Text("Show AnotherListView")
}
}
}
}
The list view of the objects:
struct ObjectsListView : View {
#Environment(\.myObjectsStore.objects) var myObjectsStores: Places
var body: some View {
Group {
Section {
ForEach(myObjectsStore.objects) { object in
NavigationLink(destination: ObjectDetailView(object: object)) {
ObjectCell(object: object)
}
}
}
Section {
// this little boi
PresentationButton(destination: ObjectDetailView(objectToEdit: MyObject(store: myObjectsStore))) {
Text("Add New Object")
}
}
}
}
}
The detail view:
struct ObjectsDetailView : View {
#Binding var myObject: MyObject
var body: some View {
Text("\(myObject.title)")
}
}
So the problem is quite complex.
The ObjectsListView creates instance of the MyObject(store: myObjectsStore) on itself initialization while computing body.
The MyObject object is setting its store property on itself initialization, since it should know is it belongs to myObjectsStore or to anotherMyObjectsStore.
The myObjectsStore are #BindableObjects since their changes are managing by SwiftUI itself.
So this behavior ends up that I've unexpected MyObject() initializations since the Views are computing itself. Like:
First MyObject creates on the ObjectsListView initialization.
Second MyObject creates on its PresentationButton pressing (the expected one).
Third (any sometimes comes even fourth) MyObject creates on dismissing ObjectsDetailView.
So I can't figure what pattern should I use this case to create only one object?
The only thing that I'd come to is to make the follow code:
struct ObjectsListView : View {
#Environment(\.myObjectsStore.objects) var myObjectsStores: Places
#State var buttonPressed = false
var body: some View {
Group {
if buttonPressed {
ObjectDetailView(objectToEdit: MyObject(store: myObjectsStore))
} else {
Section {
ForEach(myObjectsStore.objects) { object in
NavigationLink(destination: ObjectDetailView(object: object)) {
ObjectCell(object: object)
}
}
}
Section {
Button(action: {
self.buttonPressed.toggle()
}) {
Text("Add New Object")
}
}
}
}
}
}
Which simply redraw ObjectsListView to detail view conditionally. But it's completely out of iOS guidelines. So how to create the Only One object for another view in SwiftUI?
UPD:
Here's the project that represents the bug with Object duplication.
I'm still have no idea why the objects are duplicating in this case. But at least I know the reason yet. And the reason is this line:
#Environment(\.myObjectsStore.objects) var myObjectsStores: Places
I've tried to share my model with this wrapper to make it available in every single view (including Modal one) without passing them as an arg to the new view initializer, which are unavailable by the other ways, like #EnvironmentObject wrapper. And for some reason #Environment(\.keyPath) wrapper makes duplications.
So I'd simply replace all variables from Environment(\.) to ObjectBinding and now everything works well.
I've found the solution to this.
Here's the project repo that represents the bug with Object duplication and the version that fix this. I'm still have no idea how objects have been duplicate in that case. But I figured out why. It happens because this line:
#Environment(\.myObjectsStore.objects) var myObjectsStores: MyObjectsStore
I've used #Environment(\.key) to connect my model to each view in the navigation stack including Modal one, which are unavailable by the other ways provided in SwiftUI, e.g.: #State, #ObjectBinding, #EnvironmentObject. And for some reason #Environment(\.key) wrapper produce these duplications.
So I'd simply replace all variables from #Environment(\.) to #ObjectBinding and now almost everything works well.
Note: The code is in the rep is still creates one additional object by each workflow walkthrough. So it creates two objects totally instead of one. This behavior could be fixed by way provided in this answer.

Resources