Making a New Item in SwiftData go Directly to Edit View, or "How to Get Things Exactly Backwards"
Tags: swift, programming, learning, development
Published: 2024-05-23 03:45:13

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](https://www.hackingwithswift.com/100/swiftui) (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](https://developer.apple.com/documentation/swiftdata/adding-and-editing-persistent-data-in-your-app). 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 var`s with default values for a new `Item`.
5. If your `Item` exists, copy its property values to the `@State var`s in an `.onAppear` closure. 
6. The user edits the `@State var`s rather than directly editing the `Item`.
7. If the user clicks "Save", copy the `@State var`s into the `Item`’s properties. 
8. Or, if no `Item` exists, create a new `Item` using the `@State var`s 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!