Core Data: Beyond the Basics

Jul 26 2022 · Swift 5.5, iOS 15, Xcode 13.3.1

Part 1: Fetching & Displaying Launches

09. Challenge - Adding Tags

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 08. Compound Predicates Next episode: 10. Challenge - Displaying Tags

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 09. Challenge - Adding Tags

You’ve learned quite a bit and you haven’t really taken a break in between to let it all sink in. So before you do anything else, I want you to practice what you have learned so far with a fairly sizable challenge.

Using your app you can define RocketLaunchLists which contain a series of RocketLaunches. For the first part of the challenge I’d like you to add a Tag entity and the functionality to associate Tags with a RocketLaunch. Here are a few specifications that you can follow:

  • RocketLaunches can have multiple tags
  • A tag can be associated with multiple RocketLaunches
  • When adding tags to a RocketLaunch if a tag already exists, use that one otherwise create a new one

When you’re done, come back and I’ll walk through my solution.

You’re going to start in the data model as always. You’ll add a new Entity and give it the name Tag. A Tag is fairly simple and has a title attribute of type String that is required. The Tag entity also has a relationship.

The relationship will be named launches, have a destination of type RocketLaunch and it is going to be optional. The type of this relationship is a “To-Many” since one tag can be associated with many launches.

On the flip side, you’ll add a new relationship to the RocketLaunch type as well, named tags. This relationship has a destination of Tag and you’ll specify launches as the inverse. Mark the relationship as optional and set it as a “To-Many” type relationship.

I’m going to generate the code for the new Tag type and then switch to manual and modify some stuff myself. From the Editor menu, select add NSManagedObject Subclass, select just the Tag model and add it to your project. Then, before making any changes, you’re going to switch to the data model editor, select the Tag entity and switch the codegen to manual/none in the Data Model Inspector.

In Tag+CoreDataProperties.swift, switch the type on the launches property to a Swift set

@NSManaged public var launches: Set<RocketLaunch>

You’ll also need a static method to create a new Tag but you need to modify it slightly. Name it fetchOrCreateWith

static func fetchOrCreateWith(title: String, in context: NSManagedObjectContext) -> Tag {}

This time you’re first going to check if the tag exists - you know how to do this, you just need to combine some of the concepts you’ve learned to this point. If the tag exists you’re going to return that, otherwise you’ll create a new one. The reason you’re returning the tag is so that you can then use it when creating a new RocketLaunch object.

You’ll start with a fetch request

let request: NSFetchRequest<Tag> = fetchRequest()

You’ll notice that I’m using the type NSFetchRequest as opposed to FetchRequest. Remember that the latter, FetchRequest, is a property wrapper defined in the SwiftUI framework that combines fetching the data and updating views when the data changes. Right now we want a bit more control over what we’re doing so you’re going to use the actual fetch request type defined in the Core Data framework. Don’t worry we’ll cover it in more detail later.

Next you’re going to add a predicate. You want to fetch tags where the tag’s title equals the title being passed in as an argument.

let predicate = NSPredicate(format: "%K == %@", "title", title.lowercased())

The key value specifier you’ll use is title, since you want to match on the tag’s title. For the object value, you’re going to pass in the lowercased title. You’ll want to make sure you’re calling the lowercased method otherwise the tag name work with an uppercase W will create a separate tag than if it were lowercase W. By always lowercasing it you’ll make sure there are no duplicates.

Add the predicate to the fetch request

request.predicate = predicate

Now you can execute the fetch to get any matching tags.

do {

} catch {
  fatalError("Error fetching tags")
}

So far you’ve only used property wrappers to execute a fetch but the context can do it for you directly as well.

let results = try context.fetch(request)

You’ll check the results to see if there are tags and if there are you’ll grab the first one

if let tag = results.first {
  return tag
}

If there aren’t any, then you can create a new one.

else {

}

The process here is the one you’ve been using so far - you’ll insert the tag into the context and modify its attributes. Again you’ll make sure you lowercase the title.

let tag = Tag(context: context)
tag.title = title.lowercased()
return tag

Still a couple more changes left to make. In RocketLaunch+CoreDataProperties, let’s add the new tags relationship

@NSManaged var tags: Set<Tag>?

and modify the createWith method to accept tags. I’m going to give the parameter a default value so that the code compiles without having to change the method call site wherever you used it.

//static func createWith(
//	name: String,
//	notes: String,
//	launchDate: Date,
//	isViewed: Bool,
//	launchpad: String,
	tags: Set<Tag> = [],
//	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.tags = tags
//	launch.addToList(list)
	
//	do {
//	  try managedObjectContext.save()
//	} catch {
//	  let nserror = error as NSError
//	  fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
//	}
//}

Now for the UI. Let’s keep this super simple. Navigate to the LaunchCreateView and right above the Section view with launch date selection, add another Section with a single text field.

Section {
  TextField("Tags", text: $tags)
}

You’ll also need a corresponding state property

@State var tags: String = ""

In the Save button’s action closure, let’s modify the code to save a series of tags. We’re going to assume that users input tags as a single string with tags separated by comma.

let tags = Set(self.tags.split(separator: ",").map { Tag.fetchOrCreateWith(title: String($0), in: self.viewContext) })

Here I’m iterating over the tags string, splitting by comma and then mapping over each and using the fetchOrCreate method you defined earlier. You need to use the String initializer when passing the title in as an argument because the split method returns an array of substrings.

Now you can add it to a launch

//RocketLaunch.createWith(
//	name: self.text,
//	notes: self.notes,
//	launchDate: self.launchDate,
//	isViewed: false,
//	launchpad: self.launchPad,
	tags: tags,
//	in: self.launchList,
//	using: self.viewContext)

Build and run the app. Create a new launch

List: Test Flights
Launch title: Test flight 1
Tags: SpaceX, texas, test

When you hit save, you now have tags associated with your rocket launch. If you’re ready, let’s move on to part 2 of the challenge!