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

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.

Related

SwiftUI - IOS 16 - How to use new NavigationStack and NavigationPath for programatic navigation in MVVM architecture?

Description
For programatic navigation you could previously use NavigationLink(isActive:, destination:, label:) which would fire navigation when the isActive param is true. In IOS 16 this became deprecated and NavigationStack, NavigationLink(value:, label:) and NavigationPath was introduced.
To read about the usage of these follow the links:
https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
https://www.hackingwithswift.com/articles/250/whats-new-in-swiftui-for-ios-16 (search for NavigationStack)
My question is how should I use and maintain the array with the content of the navigation stack (like the NavigationPath object) if I'd like to use it in different Views and in their ViewModels?
As you can see in the code below I created a NavigationPath object to hold my navigation stack in the BaseView or BaseView.ViewModel. This way I can do programatic navigation from this BaseView to other pages (Page1, Page2), which is great.
But if I go to Page1 and try to navigate from there to Page2 programatically I need to have access to the original NavigationPath object object, the one that I use in BaseView.
What would be the best way to access this original object?
It is possible that I misunderstand the usage of this new feature but if you have any possible solutions for programatic navigation from a ViewModel I would be glad to see it :)
Code
What I've tried to do:
struct BaseView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationStack(path: $viewModel.paths) {
VStack {
Button("Page 1", action: viewModel.goToPage1)
Button("Page 2", action: viewModel.goToPage2)
}
.navigationDestination(for: String.self) { stringParam in
Page1(stringParam: stringParam)
}
.navigationDestination(for: Int.self) { intParam in
Page2(intParam: intParam)
}
}
}
}
extension BaseView {
#MainActor class ViewModel: ObservableObject {
#Published var paths = NavigationPath()
func goToPage1() {
let param = "Some random string" // gets the parameter from some calculation or async network call
paths.append(param)
}
func goToPage2() {
let param = 19 // gets the parameter from some calculation or async network call
paths.append(param)
}
}
}
struct Page1: View {
#StateObject var viewModel = ViewModel()
let stringParam: String
var body: some View {
VStack {
Button("Page 2", action: viewModel.goToPage2)
}
}
}
extension Page1 {
#MainActor class ViewModel: ObservableObject {
func goToPage2() {
// Need to add value to the original paths variable in BaseView.ViewModel
}
}
}
struct Page2: View {
#StateObject var viewModel = ViewModel()
let intParam: Int
var body: some View {
Text("\(intParam)")
}
}
extension Page2 {
#MainActor class ViewModel: ObservableObject {
}
}
There is no need for MVVM in SwiftUI because the View struct plus property wrappers is already equivalent to a view model object but faster and less error prone. Also in SwiftUI we don't even have access to the traditional view layer - it takes our View data structs, diffs them to create/update/remove UIView/NSView objects, using the best ones for the platform/context. If you use an object for view data instead, then you'll just have the same consistency problems that SwiftUI was designed to eliminate.
Sadly the web (and Harvard University) is filled with MVVM SwiftUI articles by people that didn't bother to learn it properly. Fortunately things are changing:
I was wrong! MVVM is NOT a good choice for building SwiftUI applications (Azam Sharp)
How MVVM devs get MVVM wrong in SwiftUI: From view model to state (Jim Lai)
Stop using MVVM for SwiftUI (Apple Developer Forums)

Can't show view in Swift UI with layers of views and onAppear

There is a strange case where if you show a view through another view the contents (list of 3 items) of the second view won't show when values are set using onAppear. I'm guessing SwiftUI gets confused since the second views onAppear is called prior to the first views onAppear, but I still think this is weird since both of the views data are only used in their own views. Also, there is no problem if I don't use view models and instead have the data being set using state directly in the view, but then there is yet another problem that the view model declaration must be commented out otherwise I get "Thread 1: EXC_BAD_ACCESS (code=1, address=0x400000008)". Furthermore, if I check for nil in the first view on the data that is set there before showing the second one, then the second view will be shown the first time you navigate (to the first containing the second), but no other times. I also tried removing content view and starting directly at FirstView and then the screen is just black. I want to understand why these problems happen, setting data through init works but then the init will be called before it's navigated to since that's how NavigationView works, which in turn I guess I could work around by using a deferred view, but there are cases where I would like to do stuff in the background with .task as well and it has the same problem as .onAppear. In any case I would like to avoid work arounds and understand the problem. See comments for better explanation:
struct ContentView: View {
var body: some View {
NavigationView {
// If I directly go to SecondView instead the list shows
NavigationLink(destination: FirstView()) {
Text("Go to first view")
}
}
}
}
class FirstViewViewModel: ObservableObject {
#Published var listOfItems: [Int]?
func updateList() {
listOfItems = []
}
}
struct FirstView: View {
#ObservedObject var viewModel = FirstViewViewModel()
// If I have the state in the view instead of the view model there is no problem.
// Also need to comment out the view model when using the state otherwise I get Thread 1: EXC_BAD_ACCESS runtime exception
//#State private var listOfItems: [Int]?
var body: some View {
// Showing SecondView without check for nil and it will never show
SecondView()
// If I check for nil then the second view will show the first time its navigated to, but no other times.
/*Group {
if viewModel.listOfItems != nil {
SecondView()
} else {
Text("Loading").hidden() // Needed in order for onAppear to trigger, EmptyView don't work
}
}*/
// If I comment out onAppear there is no problem
.onAppear {
print("onAppear called for first view after onAppear in second view")
viewModel.updateList()
// listOfItems = []
}
}
}
class SecondViewViewModel: ObservableObject {
#Published var listOfItems = [String]()
func updateList() {
listOfItems = ["first", "second", "third"]
}
}
struct SecondView: View {
#ObservedObject var viewModel = SecondViewViewModel()
// If I set the items through init instead of onAppear the list shows every time
init() {
// viewModel.updateList()
}
var body: some View {
Group {
List {
ForEach(viewModel.listOfItems, id: \.self) { itemValue in
VStack(alignment: .leading, spacing: 8) {
Text(itemValue)
}
}
}
}
.navigationTitle("Second View")
.onAppear {
viewModel.updateList()
// The items are printed even though the view don't show
print("items: \(viewModel.listOfItems)")
}
}
}
We don't use view model objects in SwiftUI. For data transient to a View we use #State and #Binding to make the View data struct behave like an object.
And FYI initing an object using #ObservedObject is an error causing a memory leak, it will be discarded every time the View struct is init. When we are creating a Combine loader/fetcher object that we want to have a lifetime tied to the view we init the object using #StateObject.
Also you must not do id: \.self with ForEach for an array of value types cause it'll crash when the data changes. You have to make a struct for your data that conforms to Identifiable to be used with ForEach. Or if you really do want a static ForEach you can do ForEach(0..<5) {

Using a design pattern like MVC with SwiftUI

I am trying to implement a design pattern like MVC in order to achieve low coupling between different parts of the code. There are few materials online that I personally didn't find helpful in relation to IOS or swift UI development and MVC pattern.
What I am trying to understand is how should the controller class control or render the UI in Swift UI ?
Following the MVC pattern for example - the View shouldn't know about how the model looks like, so sending an object back from the Data Base to the view in order to visually present it wouldn't be a good Idea..
Say we have the following View and Controller, how should I go about the interaction between the controller and view when sending the data back from the DB in order to visually present it in the view ?
View:
import SwiftUI
import Foundation
struct SwiftUIView: View {
var assignmentController = AssignmentController()
#State var assignmentName : String = ""
#State var notes : String = ""
var body: some View {
NavigationView {
VStack {
Form {
TextField("Assignment Name", text: $assignmentName)
TextField("Notes", text: $notes)
}
Button(action: {
self.assignmentController.retrieveFirstAssignment()
}) {
Text("Get The First Assignment !")
}
}
.navigationBarTitle("First Assignment")
}
}
}
Controller
var assignmentModel = AssignmentModel()
func retrieveFirstAssignment()
{
var firstAssignment : Assignment
firstAssignment=assignmentModel.retrieveFirstAssignment()
}
For now, it does nothing with the object it found.
Model
An object in the model composed of two String fields : "assignmentName" and "notes".
*We assume the assignment Model have a working function that retrieves one task from the DB in order to present it in the view.
struct SwiftUIView: View {
#State m = AssignmentModel()
var body: some View {
// use m
}
func loadAssignmentFromDB() {
m.retrieveFirstAssignment()
}
}
This is what I called "enhanced MVC with built-in MVVM".
It satisfies your pursuit of MVC and possibly MVVM, at the same time, with much less effort.
Now I'll argue why:
function, or more specifically, mutation is Control. you don't need an object called "Controller" to have C in MVC. Otherwise we might as well stick to UIKit for 10 more years.
this makes sense when your model is value type. nothing can mutate it without your specific say-so, e.g.; #State annotation. since the only way to mutate model is via these designated endpoints, your Control takes effect only in the function that mutates these endpoints.
Quoting from another reply:
It is true that SwiftUI is a much closer match with MVVM than with MVC. However, almost all example code in the Apple documentation is so simple the ViewModel (and/or Controller in MVC) is left out completely. Once you start creating bigger projects the need for something to bridge your Views and Models arises. However, IMO, the SwiftUI documentation does not (yet) fully address this in a satisfying way.
SwiftUI, if anything, enhances MVC. What is the purpose of having ViewModel?
a.) to have model view binding, which is present in my code snippet above.
b.) to manage states associated with object. if #State does not give you the impression that it is for you to manage state, then i don't know what will. it's funny how many MVVM devs are just blind to this. Just as you don't need View Controller for Control, you don't need ViewModel for VM. Design pattern is a #State of mind. Not about specific naming and rigid structure.
c.) say i'm open to MVVM without being based. which code snippet do you think have more chance in scaling to larger projects? my compact one or that suggested in another reply?
hint: think how many extra files, view models, observableobjects, glue codes, pass-around-vm-as-parameters, init function to accept vm as parameters, you are going to have. and these are just for you to write some of the code in another object. it says nothing about reducing or simplifying the task at hand. hell it does even tell you how to refactor your control codes, so you are most likely to just repeat whatever you did wrong in MVC all over again. did i mention ViewModel is a shared, reference type object with implicit state managements? so what's the point of having a value type model when you are just going to override it with a reference type model?
it's funny how MVVM devs say SwiftUI in its base form is not scalable to larger projects. keeping things simple is the only way to scale.
This is what I observed as the roadmap of dev progression in 2020.
Day1: beginner
Day2: google some, drop MVC
Day3: google some more, SwiftUI not scalable
Day4: OK, I need MVVM+RxSwift+Coordinator+Router+DependencyInjection to avoid SDK short-comings.
My suggestion, due to this seems like a common beginner question, is to learn to walk before you run.
I've personally seen RxSwift developers move controller code to view so that controller appears "clean", and need 3 third-party libraries (one is a custom fork) to send a http GET.
Design pattern means nothing if you can't get simple things simple.
To me this is a very good question. It is true that SwiftUI is a much closer match with MVVM than with MVC. However, almost all example code in the Apple documentation is so simple the ViewModel (and/or Controller in MVC) is left out completely. Once you start creating bigger projects the need for something to bridge your Views and Models arises. However, IMO, the SwiftUI documentation does not (yet) fully address this in a satisfying way. I would love other developers to correct me or expand on this (I'm still learning), but here's what I found out so far:
For managing updating your views in non-example project you almost always want to use ObservableObject/ObservedObject.
Views should only observe an object if they need to be updated if it changes. It is better if you can delegate the updates to a child view.
It may be tempting to create a large ObservableObject and add #Published for all of its properties. However, this means that a view that observes that object gets updated (sometimes visibly) even if a property changes on which the view does not even depend.
Binding is the most natural interface for Views that represent controls that can modify data. Beware that a Binding does NOT trigger updating views. Updating the view should be managed either #State or #ObservedObject (this can done by the parent view of the control).
Constants are the natural interface for Views that only display data (and not modify it).
Here is how I would apply this to your example:
import SwiftUI
//
// Helper class for observing value types
//
class ObservableValue<Value: Hashable>: ObservableObject {
#Published var value: Value
init(initialValue: Value) {
value = initialValue
}
}
//
// Model
//
struct Assignment {
let name : String
let notes: String
}
//
// ViewModel?
//
// Usually a view model transforms data so it is usable by the view. Strings are already
// usable in our components. The only change here is to wrap the strings in an
// ObservableValue so views can listen for changes to the individual properties.
//
// Note: In Swift you often see transformations of the data implemented as extensions to
// the model rather than in a separate ViewModel.
class AssignmentModelView {
var name : ObservableValue<String>
var notes: ObservableValue<String>
init(assignment: Assignment) {
name = ObservableValue<String>(initialValue: assignment.name)
notes = ObservableValue<String>(initialValue: assignment.notes)
}
var assignment: Assignment {
Assignment(name: name.value, notes: notes.value)
}
}
//
// Controller
//
// Publish the first assignment so Views depending on it can update whenever we change
// the first assignment (**not** update its properties)
class AssignmentController: ObservableObject {
#Published var firstAssignment: AssignmentModelView?
func retrieveFirstAssignment() {
let assignment = Assignment(name: "My First Assignment", notes: "Everyone has to start somewhere...")
firstAssignment = AssignmentModelView(assignment: assignment)
}
}
struct ContentView: View {
// In a real app you should use dependency injection here
// (i.e. provide the assignmentController as a parameter)
#ObservedObject var assignmentController = AssignmentController()
var body: some View {
NavigationView {
VStack {
// I prefer to use `map` instead of conditional views, since it
// eliminates the need for forced unwrapping
self.assignmentController.firstAssignment.map { assignmentModelView in
Form {
ObservingTextField(title: "Assignment Name", value: assignmentModelView.name)
ObservingTextField(title: "Notes", value: assignmentModelView.notes)
}
}
Button(action: { self.retrieveFirstAssignment() }) {
Text("Get The First Assignment !")
}
}
.navigationBarTitle("First Assignment")
}
}
func retrieveFirstAssignment() {
assignmentController.retrieveFirstAssignment()
}
}
//
// Wrapper for TextField that correctly updates whenever the value
// changes
//
struct ObservingTextField: View {
let title: String
#ObservedObject var value: ObservableValue<String>
var body: some View {
TextField(title, text: $value.value)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This may be overkill for your app. There is a more straightforward version, but it has the
disadvantage that TextFields are updated even though their contents hasn't changed. In
this particular example I do not think that matters much. For larger projects it may
become important, not (just) for performance reasons, but updates are sometimes very
visible. For reference: here's the simpler version.
import SwiftUI
// Model
struct Assignment {
let name : String
let notes: String
}
// ViewModel
class AssignmentViewModel: ObservableObject {
#Published var name : String
#Published var notes: String
init(assignment: Assignment) {
name = assignment.name
notes = assignment.notes
}
}
// Controller
class AssignmentController: ObservableObject {
#Published var firstAssignment: AssignmentViewModel?
func retrieveFirstAssignment() {
let assignment = Assignment(name: "My First Assignment", notes: "Everyone has to start somewhere...")
firstAssignment = AssignmentViewModel(assignment: assignment)
}
}
struct ContentView: View {
// In a real app you should use dependency injection here
// (i.e. provide the assignmentController as a parameter)
#ObservedObject var assignmentController = AssignmentController()
var body: some View {
NavigationView {
VStack {
self.assignmentController.firstAssignment.map { assignmentModelView in
FirstAssignmentView(firstAssignment: assignmentModelView)
}
Button(action: { self.retrieveFirstAssignment() }) {
Text("Get The First Assignment !")
}
}
.navigationBarTitle("First Assignment")
}
}
func retrieveFirstAssignment() {
assignmentController.retrieveFirstAssignment()
}
}
struct FirstAssignmentView: View {
#ObservedObject var firstAssignment: AssignmentViewModel
var body: some View {
Form {
TextField("Assignment Name", text: $firstAssignment.name)
TextField("Notes", text: $firstAssignment.notes)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I had the very same question and I came accross an excellent paper by Matteo Manferdini in which he describes how to use the MVC model in SwiftUI.
I used his paper to refactor a Pizza app that uses CoreData. And even though I’m still a beginner in SwiftUI it gave me a very good understanding of how to implement MVC. You can find Matteo’s paper here.

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...
}

Resources