Leave a rating/review
Welcome back! Now that you have a more complex object graph, you have to rework the app a bit to handle it. First, you’ll need to go to your simulator and get rid of the app on the launch screen. Any RocketLaunches you created prior to the last video are incompatible with the new model - remember that instances of RocketLaunch are now required to have an associated list. If you build and run, the app will try to look for that list and crash instead.
If this were code running in production as a real app, you would go about this in a completely different manner by undertaking a model migration, but for our purposes just deleting the app suffices. With the simulator open, press and hold to bring up the modal menu and delete the app.
Next, you’re going to grab the starter files for this video. The UI has been modified to present a LaunchList view up front, from which you can drill down into individual launches.
Build and run the app. Make sure everything is as expected. You should see a list of lists when it loads, full of placeholder data. Right now if you tap on any of these lists nothing happens and that’s because you have some work to do!
First up, you need to add functionality to create a list. You implemented the logic in the last video, so let’s wire it all together. Navigate to ListCreateView.swift and at the very bottom you should see a save Button defined in the nav bar. Above the call to dismiss(), put a call to the function you defined earlier
RocketLaunchList.create(withTitle: self.text, in: self.viewContext)
Nice and easy. Now navigate back to ListView to replace the placeholder data with actual RocketLaunchLists that you save. The last time you did this you created a static property on the RocketLaunch type that returns a fetch request but there’s another way you can achieve this using a property wrapper.
At the top of the struct, after the Environment property declare a FetchRequest property.
@FetchRequest(sortDescriptors: [])
var launchLists: FetchedResults<RocketLaunchList>
As mentioned earlier this is no different from you what did before. Instead of using the property wrapper, you created an instance of the underlying struct FetchRequest, and then accessed the wrappedValue property on the property observer to get the returned results.
Here you’re using the property wrapper, well, as a property wrapper. The declaration is a fetch request and the resulting type is a fetched result, but it works in the exact same way.
Now you can replace the placeholder data in the ForEach view with the fetched results.
ForEach(launchLists, id: \.self) { launchList in }
and inside the closure use the launch list’s title property to display the title.
Text(launchList.title ?? "")
Let’s see if this worked! Build and run the app. The view should now be empty. Ok, everything looks good. Tap on the plus button in the top right to create a new list.
Enter a title - Florida Launches and hit save. You should see a new launch list added to the view. Now when you tap on one of these rows to select a list you should navigate to the screen that shows you all the launches for that particular list.
To implement that transition to the LaunchesView you need to wrap the row in a NavigationLink. Replace the Text and its contents with
NavigationLink(destination: LaunchesView()) {
CircularImageView(color: .red)
Text(launchList.title)
}
You’re specifying the LaunchesView as the destination for any row. This won’t work as it is. Right now the LaunchesView is set up to fetch all launches but you want to restrict to just the launches that are associated with the list you tapped on. That’s where the relationship you defined comes into play.
First, navigate to LaunchesView and add a stored property
let launchList: RocketLaunchList
You’re not going to give it a value because you want to pass this in when you tap on the row. You’ll need to fix the preview code so that it compiles first.
let context = PersistenceController.preview.container.viewContext
let newLaunchList = RocketLaunchList(context: context)
newLaunchList.title = "Preview List"
return LaunchesView(launchList: newLaunchList).environment(\.managedObjectContext, context)
Ok now back in ListView pass the launches list through to the LaunchesView.
NavigationLink(destination: LaunchesView(launchList: launchList)) {}
Next you need to figure out how to fetch the launches associated with this list. Before you can do that though you need to modify how you save RocketLaunches to make sure you’re saving it to a list. Navigate to RocketLaunch+CoreDataProperties and modify the create method to accept a launch list.
static func createWith(
name: String,
notes: String,
launchDate: Date,
isViewed: Bool,
launchpad: String,
** in list: RocketLaunchList,
using managedObjectContext: NSManagedObjectContext
) {
let launch = RocketLaunch(context: managedObjectContext)
launch.name = name
launch.notes = notes
launch.launchDate = launchDate
launch.isViewed = isViewed
launch.launchpad = launchpad
** launch.list = list
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
You’ll get an error when you do this and that’s because there isn’t a list property modeled on RocketLaunch. Go ahead and add that:
@NSManaged var list: RocketLaunchList
Back in LaunchCreateView, let’s define a RocketLaunchList property that we initialize this view with and then you can pass in the list to the method to ensure that this launch is associated with a particular list.
let launchList: RocketLaunchList
RocketLaunch.createWith(
name: self.text,
notes: self.notes,
launchDate: self.launchDate,
isViewed: false,
launchpad: self.launchPad,
in: self.launchList,
using: self.viewContext)
You’ll need to fix the preview code again to get everything to compile.
let context = PersistenceController.preview.container.viewContext
let newLaunchList = RocketLaunchList(context: context)
newLaunchList.title = "Preview List"
return LaunchCreateView(launchList: newLaunchList)
.environment(\.managedObjectContext, context)
All that’s left to do is pass the RocketLaunchList all the way through. First in the NewLaunchButton view in LaunchesView.swift
let launchList: RocketLaunchList
LaunchCreateView(launchList: self.launchList)
and then in LaunchesView
NewLaunchButton(isShowingCreateModal: $isShowingCreateModal, launchList: self.launchList)
Build and run the app. Now you can tap into a list and create a RocketLaunch
Triple Launch
Launch Date: 6/1/22
When you hit save, the rocket launch should automatically be added to the list. But the implementation is not correct. Here’s why. Let’s go back to the lists view and add a new list
CA Launches
Once you hit save and the RocketLaunchList appears in the view, tap into it and you should see the triple launch in there. Clearly you’re still fetching all launches and not just the ones you need for a given list.
Navigate to LaunchesView and in the ForEach view change it from launches, to launchList.launches.
Build and run the app. Now the only the launches created for that specific list show up. You can also add a new RocketLaunch and when you hit save it will appear on your list.
Unfortunately there’s still a lingering bug. If you tap on a RocketLaunch to mark it is as viewed the RocketLaunch is not removed off the list. This seems to be an issue with how Core Data and SwiftUI work together.
Previously the reason that it all worked was because you were using the fetch request directly on the RocketLaunch entity to drive the view logic. When the view state changed, SwiftUI and Core Data worked together to refetch the new launches or remove viewed ones and update the view.
In this case you’re not actually using the fetch request on the RocketLaunch entity to drive the view. You’re using the underlying RocketLaunchList and the relationship you defined to populate the views.
For whatever reason it seems like Core Data is not propagating a change to SwiftUI when the change is on an entity that is defined through a relationship. There are workarounds and in the next video let’s look at how predicates can help us solve the problem.