SwiftUI DocumentGroup and switching to background - ios

I have made an app based on the new SwiftUI multi platform target for a "Document based app".
However, I face weird issues. As long as an app is in the foreground, it works just fine. If it is moved to the background by task switching, and then again to the foreground, mutations are being saved to the document, but the SwiftUI Views don't receive mutations. So whenever you press a button in the UI that mutates the document you see nothing happening while the mutation is there once you reload the document from disk.
So i am thinking, I use ObservedObjects, they probably get kicked out of memory once I move to the background. could this be the cause of my bug?
But then I added a print line to the App struct.
import SwiftUI
#main
struct MyApp: App {
fileprivate func myLogging(_ file: FileDocumentConfiguration<MyDocument>) -> some View {
print("""
IT IS CALLED
""")
return MainView().environmentObject(BindingWrapper(file.$document))
}
var body: some Scene {
DocumentGroup(newDocument: MyDocument()) { (file) in
return myLogging(file)
}.commands { AppCommands() }
}
}
and guess what... this print always executes just before a mutation is being rendered. Which makes sense. because file.$document is a binding, and if you do a mutating action, the binding will warn Apple that the file is dirty, but it will also invalidate the entire hierarchy. This logging will still print once the bug has occurred!
So on the line MainView().environmentObject(BindingWrapper(file.$document)) I assume everything is created from scratch. BindingWrapper is a custom class I made to convert a binding in an observable object. And this is one of the objects I worried about, that they might be freed. but if they are created newly.... they should be always there, right?
And by the way, this object is owned by the environment. So it should not be freed.
So, now I am stuck. is Apple doing some clever caching on bindings / ObservedObjects which will inject old objects into my view hierarchy even though I think everything is created newly?

Try moving any wiring/instantiation to the first view of the document group. If that view houses StateObjects you expect to share the lifetime of the document window, they will not be rebuilt.
In the example below, a WindowStore is housed as an #StateObject as described. A RootStore housed in App creates the WindowStore, which includes vending services and registering it in a managed array of windows. Either could enable your logging service. (For me, that array helps WindowGroups operate on a specific document when #FocusedValue would fail (i.e., the top-most document is no longer the key window).)
#main
struct ReferenceFileDoc: App {
#StateObject var root: RootStore
var body: some Scene {
DocumentGroup { ProjectDocument() } editor: { doc in
DocumentGroupRoot(
window: root.makeWindowStore(doc.document),
factory: SwiftUIFactory(root, doc.document)
)
.environmentObject(doc.document)
.environment(\.documentURL, doc.fileURL)
.injectStores(from: root)
}.commands { Menus(root: root) }
.... other scenes ...
struct DocumentGroupRoot: View {
#EnvironmentObject var doc: ProjectDocument
#Environment(\.undoManager) var undoManager
#Environment(\.documentURL) var url
#StateObject var window: WindowStore
#StateObject var factory: UIFactory
var body: some View {
passUndoManagerToDocument()
factory.reference(window)
return DocumentWindow(vm: factory.makeThisVM()) // Actual visible window
.focusedValue(\.keyWindow, window)
.focusedValue(\.keyDocument, doc)
.onAppear { /// Tasks }
.reportHostingNSWindow { [weak window] in
window?.setWindow($0)
}
.onChange(of: url) { [weak window] in window?.setFileURL($0) }
.environmentObject(/// sub-state stores from WindowStore)
.environmentObject(window)
.environmentObject(factory)
}
}

Related

How can I invalidate a SwiftUI View in response to a notification?

I'm new to SwiftUI and trying to start using it in a complex, existing UIKit app. The app has a theming system, and I'm not sure how to get the SwiftUI view to respond to theme change events.
Our theme objects look like
class ThemeService {
static var textColor: UIColor { get }
}
struct ViewTheme: {
private(set) var textColor = { ThemeService.textColor }
}
where the value returned ThemeService.textColor changes when the user changes the app's theme. In the UIKit portions of the app, views observe a "themeChanged" Notification and re-read the value of the textColor property from their theme structs.
I'm not sure how to manage this for SwiftUI. Since ViewTheme isn't an object, I can't use #ObservableObject, but its textColor property also doesn't change when the theme changes; just the value returned by calling it changes.
Is there a way to somehow get SwiftUI to re-render the view hierarchy from an external event, rather than from a change in a value that the view sees? Or should I be approaching this differently?
Your answer works perfectly well, but it requires adoption of ObservableObject. Here is an alternative answer which uses your existing NotificationCenter notifications to update a SwiftUI view.
struct MyView: View {
#State private var textColor: UIColor
var body: some View {
Text("Hello")
.foregroundColor(Color(textColor))
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("themeChanged"))) { _ in
textColor = ThemeService.textColor
}
}
}
This requires a #State variable to hold the theme's current data, but it's correct because a SwiftUI view is really just a snapshot of what the view should currently display. It ideally should not reference data that is arbitrarily changed because it leads to data-sync problems like the question you asked. So in a SwiftUI view, it is problematic to write ThemeService.textColor directly within the body unless you are certain an update will always occur after it the theme gets changed.
I was able to get this working by cheating with the theming system a little and changing the ViewTheme to an object:
class ViewTheme: ObservableObject {
private(set) var textColor = { ThemeService.textColor }
init() {
NotificationCenter.default.addObserver(forName: Notification.Name("themeChanged"),
object: nil, queue: .main) { [weak self] _ in
self?.objectWillChange.send()
}
}
}
Now the view can mark it with #ObservedObject and will be re-generated when the "themeChanged" notification is fired.
This seems to be working great, but I'm not sure if there are non-obvious problems that I'm missing with this solution.

Reset main content view - Swift UI

My app has one main screen that the user uses, then once they're done go to another view, currently implemented as a .fullscreencover. I want the user to be able to press a button and the app pretty much resets, turning everything back to the way it is when the app is launched for the first time and resetting all variables and classes.
The one method I have tried is opening the view again on top of the final view, however this doesn't reset it. Here is the code I have tried but doesn't work:
Button("New Game"){
newGame.toggle()
}
.fullScreenCover(isPresented: $newGame){
ContentView()
}
Alongside this I have tried navigation views however this causes more issues with the functionality of my app.
Is there a line of code that allows you to do this?
The possible approach is to use global app state
class AppState: ObservableObject {
static let shared = AppState()
#Published var gameID = UUID()
}
and have root content view be dependent on that gameID
#main
struct SomeApp: App {
#StateObject var appState = AppState.shared // << here
var body: some Scene {
WindowGroup {
ContentView().id(appState.gameID) // << here
}
}
}
and now to reset everything to initial state from any place we just set new gameID:
Button("New Game"){
AppState.shared.gameID = UUID()
}

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.

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