UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How to keep the model object up-to-date for a .sheet that modifies the model?

Forums > SwiftUI

@jgale  

Hi all! Long time listener, first time caller. This is a bit tough to simplify, but I'll try to describe the problem I'm trying to solve.

Use Case

My app has a List that displays Task objects. When the user taps a Task, I show a TaskDetailView it in a modal .sheet. In this sheet, I allow the user to make changes to the model object itself. (It does this by updating a TaskRepository, which updates data in Firebase, which updates my @Published array of tasks that the List displays.)

The Code

When the user taps a TaskCell, I set shownTask like so:

    @State var showSheet = false
    @State var shownTask = Task.placeholderTask

    ...

    List {
        ForEach(self.viewModel.sections, id: \.name) { section in
            Section(header: RowHeader(title: section.name)) {
                ForEach(section.tasks) { task in
                    Button(action: {
                        self.viewModel.shownTask = task
                        self.showSheet.toggle()
                    }) {
                        TaskCell(viewModel: TaskViewModel(task: task))
                    }
                }
            }
        }
    }
    .sheet(isPresented: $showSheet) {
        TaskDetailView(
            viewModel: TaskViewModel(task: self.viewModel.shownTask),
            showSheet: self.$showSheet)
    })

The Problem

When I make changes to the underlying TaskRepository everything gets updated in the list properly. However, my shownTask state variable is stale, it contains the original version of the Task before the changes were made. This causes my TaskDetailView to immediatley revert and not show the changes that were just made.

How can I keep that shownTask variable up-to-date?

Ideas

  • I could just store the ID of the shownTask instead, and initialize my TaskViewModel with that, and then update it from the TaskRepository. This does work, but it sort of ruins the cleanliness of my TaskDetailView because I'd prefer to just pass in an actual Task and have it reflect that. For example, this basically breaks my Xcode Preview functionality because now I'm relying on having a TaskRepository and Firebase stuff set up.
  • Maybe I could use Combine to somehow keep this shownTask up to date when underlying changes are made to the model? I am struggling with how I would set this up. I don't think it would work as a @State parameter anymore. It seems like I would have to keep track of my shownTask deep in my TaskRepository and make it @Published... it seems to be sort of blurring the lines between models and views.

Anyone have any other ideas or suggestions? Thank you!

3      

hi,

i'll weigh in on this with support for your question, and with the hope that someone can really answer this, yet i will offer two thoughts (which may or may not provide any comfort). i have often struggled with the problem that you describe.

(1) there is a concept of an "ObservableArray," which tacks on an AnyCancellable to every item in the array it holds and then passes any incoming objectWillChange message to be an objectWillChange message from the array to anyone downstream. Combine is not anything i feel comfortable with just yet, although i found something out on Stack Overflow some time ago about this concept of an "ObservableArray." it may require some searching on your part to find it and make sense out of it.

(2) i have an app (in a public, on-going state as i struggle with this general notion) that has basically the same structure that you describe. show a list -- go to a detail sheet and make changes -- then return -- and hope the list display updates. sometimes simply adding an @ObservedObject in the right place makes this work out fine; but in other places, everything might just work or not.

but for my use case, data comes from Core Data and, therefore, reaps a lot of benefit from the @FetchRequest property wrapper. i think that @FetchRequest does this type of ObservableArray thing under-the-hood, meaning that it's smart enough to say the array it's watching has changed if any of the objects it maintains changes.

you can find my on-going, fail-in-public project Shopping list on Github.

not sure there's an answer here, but i will certainly follow this thread.

hope that helps,

DMG

3      

If I understood the problem right, you need to annotate shownTask as an @ObservedObject:

@ObservedObject var shownTask = Task.placeholderTask

3      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Not sure I am understanding correctly, but: Is your viewModel not being refreshed when a change is made to a Task - that should cause the view to refresh and then your Task will no longer be stale in the List?

3      

@jgale  

Thanks for the replies folks. It sounds like I didn't do a create job describing the problem which I expected, it's kind of hard to distill it to a simple question.

The List itself is properly being updated. It's just the one-off individual shownTask that is not being updated because I don't know how to update it. Task is a struct and so can't be an ObservedObject. I set it via Button action when someone taps an item, but there is not going to be any way to update it, because I don't control anything when it's declarative.

Fundamentally, I don't understand how to handle something like this in a declarative form. Ideally, I would want SwiftUI to automatically update my shownTask because SwiftUI is declaratively showing the .sheet, but I guess that's not really realistic and I don't know how it would know to update it (other than the ID of the original Task tapped is the same as the shownTask).

The ObservableArray concept is interesting, I may need to look into that.

3      

Actually, I think I did understand you. I have an app that does a similar thing, and when I checked my code I was passing in the ID of the database row rather than a struct as you are. That's probably because I come from a heavily database-oriented background though, and not because I'm doing the right thing!

3      

If you want to share the Task object between screens, it must be a class. This won't work with structs because when they get passed on to the next screen, what is passed is actually a copy, so when you update it, you're updating the copy. That's why you don't see the change in your data.

3      

Can you make the task a Binding in the Detail view? I don't think that will help you actually.

3      

In your code you create a State variable, shownTask, but never use it - you use viewModel.shownTask instead. Is that correct?

3      

@jgale  

@guseulalio - I do see what you mean in that if it were a class it can be shared, and maybe that is the right way to do it. I hate to lose the value semantics for this kind of thing.

Good catch @lordmooch on the @State vs. ViewModel... I was simplifying my example to post here. Technically I do have it in my viewModel, but same thing happens when I had it as @State.

I ended up actually doing the same as you - passing the ID into the sheet, and fetching the current Task from my repo. It solves my problem. I made a 2nd initializer that can also just take a struct that I use for my Previews, so that seems to aleviate my concerns fairly well. I still feel slightly dissatisfied that I can't find a good way to keep something updated that was set by the user interaction, but I guess it's sort of the intrinsic nature of declarative style UI which I'm still learning.

Thanks for the help everyone.

3      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.