DEV Community

Monty Harper
Monty Harper

Posted on

Making a New Item in SwiftData go Directly to Edit View, or "How to Get Things Exactly Backwards"

Please note that while I do use some Swift vocabulary in this article, and while it may be helpful to someone trying to do a specific thing in Swift, it's not really a tutorial. This is more of a story about how to troubleshoot and how to learn.

I've been working on my own weird version of a to-do list app using SwiftData, and I recently found myself losing my mind over what should have been a simple bit of functionality.

The Challenge

The problem was, I wanted to make a new Item and open it in the edit view all with one tap. (Yes, I literally named my model "Item." At least I didn't call it "Thing.")

I wanted to avoid doing this the way we did it in 100DaysOfSwiftUI (highly recommended tutorials!):

  1. Make a "new item" button that creates a new item and inserts it into the model context.
  2. Use a navigation link for each item on the list to open that item in the edit view.

This requires the user to create a new item, then tap the new item to edit it. Why make my user (me) tap twice when once should be enough? If you make a new item, of course you don't want it sitting there in your list saying "look at me, I'm nothing, I'm just a new item." You're going to want to edit it right away. It can't be that difficult to make a "new item" button that creates a new item AND opens it for you in the edit view. Right? Right??

First Attempt

My foolproof plan was simple: 1) Create a new Item, 2) Send it into the edit view.

I did the obvious thing: I initialized a new Item inside my NavigationLink, which passed it to the edit view.

This appeared to work; the code compiled and didn't crash. But testing revealed some odd behavior: edits to my new items were not taking effect.

The Problem?

I have a theory: My new Item had not been inserted into the modelContext where SwiftData could deal with it properly. Somewhere between making a new Item and landing it in the edit view, I needed to inject it into SwiftData's modelContext. Then the edit view would be able to handle it properly.

Unfortunately, while you can use NavigationLink to initialize a new Item, it doesn't support doing anything with said new Item (such as injecting it into the modelContext) before passing it along. (Maybe you can see the solution here - I'll get to it later in the article.)

After struggling with that for a while, I tried injecting the new Item from inside the edit view itself, using an .onAppear closure. This also appeared to work, however I got the same weird behavior as before. I'm guessing that's because the .onAppear happens after the view is initialized, so I was injecting the new Item too late in the process? I don't know for sure.

All I know is SwiftUI and SwiftData were conspiring to keep my new Item out of the edit view without user intervention. Stubbornly, I still believed there had to be a way edit a new Item directly! I turned to Google for answers.

Lo and behold, the Apple Documentation for SwiftData (of all places!) includes an example showing how to do exactly what I'd been trying to do. Why didn't I start there? Huh? Why?

The Solution

I'm giving you the long version here, since Apple's approach also solves the problem of what to do if the user wants to discard changes made in the edit view. If you'd rather look at the short version, skip to the next heading.

Here's the way I tried to do it (which wasn't working):

  1. Navigate to an edit view, passing in a new item or an existing one as a @Binding var.
  2. In an .onAppear closure, make a backup copy of the Item, and inject the Item into the modelContext.
  3. The user edits the Item directly.
  4. Do nothing if the user taps "Save."
  5. Restore the Item from its backup if the user taps “Never Mind.”

Here's what Apple says to do:

  1. In the edit view, set up your item var without a binding and make it optional - which means it's allowed to be empty.
  2. Navigate to the edit view by passing in an existing Item, or pass in nil (nothing) if you want to make a new Item.
  3. The edit view should have an @State var for each editable property of your Item.
  4. Provide your @State vars with default values for a new Item.
  5. If your Item exists, copy its property values to the @State vars in an .onAppear closure.
  6. The user edits the @State vars rather than directly editing the Item.
  7. If the user clicks "Save", copy the @State vars into the Item’s properties.
  8. Or, if no Item exists, create a new Item using the @State vars and insert the new Item into the modelContext.
  9. If the user clicks “Never Mind”, do nothing.

The Short Version

My approach:
1) Create a new Item, 2) Send it into the edit view.

Apple's approach:
1) Open the edit view with no Item, 2) Create a new Item after the user has entered some values.

I had it exactly backwards.

For a while, I thought I was missing something here. But after learning a few more things (see below), if I had to try and explain why Apple does this backwards, I believe it has more to do with the ability to undo your changes. In that aspect, Apple's approach is more elegant:

  1. You don't have to create a backup copy of the Item.
  2. You aren't making changes to the Item as you edit; all changes are written when you close the edit view. (Doing it my way, changes take effect as you make them; if that process were to be interrupted by say a dead battery, you'd lose your ability to undo your changes.)

(Also, using a binding as I did shouldn't be necessary since any Item in the modelContext will be automatically updated when changes are made.)

However, I don't believe this backwards approach is essential to getting your new Item into the ModelContext for editing. Here's why...

An Alternate Approach

A bit later, I stumbled upon another way to do this, and I wish I'd taken better notes so I could credit whatever kind soul put this out there, but at the time I didn't realize the significance of what I was looking at. I was trying to answer a different question about SwiftData, and in the example code a sample item was provided to the edit view via a computed variable.

This was a head-slapping moment for me because it's quite obvious, once you think of it.

Remember how I lamented that the NavigationLink will allow you to initialize a new Item but then you can't do anything with it other than pass it along?

Well you can do things with a new Item outside the NavigationLink, then pass it into the link; you just have to be a bit clever about it.

Here's roughly what that looks like:

  1. Make a computed variable (or a function, if you like) called "newItem" which creates a new item with default values and injects it into the ModelContext, then returns the new item.
  2. Inside the NavigationLink call for a newItem and pass it into the edit view.

There you go - the new item gets inserted into the context before it gets passed to the edit view, and you can edit it directly if you want.

What Did I Learn?

Besides the nuts and bolts of how to open a new item in the edit view with a single tap, there may be a few lessons lying around here, if I can get past my stubbornness and actually learn them!

  1. When the system seems to be telling you you can't do a thing that ought to be doable, it's time to seek outside help.
  2. The Apple documentation does include tutorials on how to do basic stuff.
  3. Learning something (in this case SwiftData) from a single source doesn't mean you've learned it thoroughly. Check out different sources on the same topic to get a fuller picture.
  4. When working with a new framework (SwiftData) things can fail silently and error messages can be unhelpful, but (again) the documentation can be a help.
  5. If the "obvious" way to do a thing fails, there's probably a "just as obvious" way to do it correctly.
  6. Find a good balance between learning and creating; moving on through the 100Days has given me tools I wish I'd had while I was struggling with this app of mine. On the other hand, struggling to create something usable keeps me engaged and motivated.

That's it for now. Happy learning!

Top comments (0)