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!):
- Make a "new item" button that creates a new item and inserts it into the model context.
- 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):
- Navigate to an edit view, passing in a new item or an existing one as a
@Binding var
. - In an
.onAppear
closure, make a backup copy of theItem
, and inject theItem
into themodelContext
. - The user edits the
Item
directly. - Do nothing if the user taps "Save."
- Restore the
Item
from its backup if the user taps “Never Mind.”
Here's what Apple says to do:
- In the edit view, set up your item
var
without a binding and make it optional - which means it's allowed to be empty. - Navigate to the edit view by passing in an existing
Item
, or pass innil
(nothing) if you want to make a newItem
. - The edit view should have an
@State var
for each editable property of yourItem
. - Provide your
@State var
s with default values for a newItem
. - If your
Item
exists, copy its property values to the@State var
s in an.onAppear
closure. - The user edits the
@State var
s rather than directly editing theItem
. - If the user clicks "Save", copy the
@State var
s into theItem
’s properties. - Or, if no
Item
exists, create a newItem
using the@State var
s and insert the newItem
into themodelContext
. - 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:
- You don't have to create a backup copy of the
Item.
- 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:
- 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. - Inside the
NavigationLink
call for anewItem
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!
- 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.
- The Apple documentation does include tutorials on how to do basic stuff.
- 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.
- 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.
- If the "obvious" way to do a thing fails, there's probably a "just as obvious" way to do it correctly.
- 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.
What's your ideal balance between learning from tutorials vs. making your own projects? Leave a comment if you like.
Happy learning!