DEV Community

loading...
Cover image for Lt. Coremander Data, pt 2

Lt. Coremander Data, pt 2

julltron profile image Joel Groomer ・8 min read

View project at this stage on GitHub here.

Core Data plus Swift helpers

In the last post, I mentioned "Core Data types" and "Swift data types," using Strings and Enums, etc. If you're familiar with Core Data, you know that this is because, wonderful as it is, Core Data just isn't very Swifty. It's been used in iOS development longer than Swift, and it's got some decidedly Objective-C architecture that can be a little confusing at first.

But Swift is just so darn flexible that we can work around some of this pretty easily! And the easiest way to do that that I've found so far is using Extensions.

Extensions are one of my favorite things about Swift. They allow you to add functionality to an existing class without subclassing it yourself. Once I realized the power of extensions, it changed the way I think about adding not-quite-there-yet features to my apps. Some day I'll write a post just about extensions. For now, let's dive into how I'm using them in this project to help work with Core Data.

Extending Codegen classes

With an entity selected in the editor (Step in this case), and the Data Model Inspector active in Xcode's right pane, you should see something like this:

Image of Xcode's Data Model Inspector

The default option is shown here for the Codegen field: Class Definition. The other options are Manual/None and Category/Extension. Since this isn't a Core Data tutorial, I won't get into what all of these are and why you'd use them, especially because for my purposes here the default is just fine.

What the Class Definition option does is create classes for your entities that you can access within Swift (or Obj-C if that's the language you're using). It does this behind the scenes. You don't have to ever see these auto-generated classes, although you can if you really want to using the Jump to definition shortcut in your editor.

The generated classes interface with Core Data for you and handle a ton of heavy lifting so that you can focus on your business logic instead of managing your persistent store. There's still a lot of work to do, but adding some really simple extensions to each of these generated classes can make that seem a whole lot easier.

Routine+CoreDataHelpers.swift

//
//  Routine+CoreDataHelpers.swift
//  Lotus Timer
//
//  Created by Joel Groomer on 11/21/20.
//  Copyright © 2020 Julltron. All rights reserved.
//

import CoreData

extension Routine {
    enum Origin: String {
        case user, downloaded
    }

    // convert Core Data types into non-optionals and standard types
    var routineAuthor: String { author ?? "" }
    var routineCategory: String { category ?? "" }
    var routineOrder: Int {
        get {
            Int(listOrder)
        } set {
            self.listOrder = Int16(newValue)
        }
    }
    var routineOrigin: Origin {
        get {
            Origin(rawValue: origin ?? "user") ?? .user
        }

        set {
            self.origin = newValue.rawValue
        }
    }
    var routineTitle: String { title ?? "New routine" }

    // convert set of Steps associated with this Routine into an array of Steps
    var routineSteps: [Step] {
        let stepsArray = steps?.allObjects as? [Step] ?? []
        return stepsArray.sorted { $0.stepOrder < $1.stepOrder }
    }


    // for SwiftUI previews and anywhere else an example Routine is needed
    static var example: Routine {
        let controller = DataController(inMemory: true)
        let viewContext = controller.container.viewContext

        let routine = Routine(context: viewContext)
        routine.author = "User"
        routine.category = "Some category"
        routine.listOrder = 0
        routine.origin = Origin.user.rawValue
        routine.title = "Example Routine"

        return routine
    }

}
Enter fullscreen mode Exit fullscreen mode

Above is the entire listing for the Routine class extension as it stands today. This handles type conversions and has some nice helper methods, and because it's an extension I can access them just like I could any of the properties or methods that Xcode generated for me.

Types - the struggle is real

Being a strongly typed language that leans very heavily on optionals, types can sometimes feel like the bane of a Swift dev's existence (until you remember that strong typing is saving you from countless potential runtime errors, and then you get over yourself). This becomes exceedingly annoying, for me anyway, when dealing with integers in Core Data. So let's start there.

Core Data and Ints-Ints-Ints

Core Data uses not one but three different integer types. If you looked closely at the images in the previous post, you may have noticed the type "Integer 16" appeared in both of my entities. The other two types are Integer 32 and Integer 64.

For those who aren't aware, there's a reason for the three different types, and that has to do with computer architecture. To simplify: 16-bit systems use 16-bit integers, and respectively so with the others.

Swift has four integer types, although most of the time we only use one: Int. The other three correspond directly to those above: Int16, Int32, and Int64. In reality, Int is going to be either the 32- or 64-bit version of the above, and at this point in time (since 2017) you could be safe assuming it's 64 if you're coding for iOS.

Because of strong typing, we either need to make sure that we are using the same integer type every time we work with an entity's attributes or we have to cast back and forth. And while you could try to simplify by making all of your entities use the same type, if you want to be at all memory-conscious it's hard to justify storing a counter that will only ever be between 0 and 10 in a 64-bit integer when 16 bits is already overkill. Xcode will remind you when you choose the wrong type, but if you're anything like me that will feel more irritating than helpful.

So the solution presented here in the extension is to set up computed properties (another amazing feature of Swift) to convert the values for you on the fly!

Code snippet of routineOrder computed property

These few lines of code create a new computed property called routineOrder that wraps the value of the entity's attribute listOrder in a getter that converts from Int16 into Int for use in Swift code and a setter that converts from Int to Int16 for storing in Core Data. Now the rest of the app, or anyone else working on the app, can forget the detail that listOrder is stored in Int16 because it doesn't matter.

I want to point out that this naming convention of using the class name followed by the attribute name (which I realize is a little wonky in this example because I used listOrder instead of just order for the attribute, but definitely tracks if you look in the Step extension) is something I got from Paul Hudson of Hacking With Swift. I adopted it because I love the simplicity in the convention, and if you adopt it across the project it's pretty easy to remember that presented with a choice between classAttribute and attribute, the former is going to be the Swiftier of the two.

Check out Paul's work at https://www.hackingwithswift.com. It's really top-notch. Our community is lucky to have him!

Stringy values

We can convert strings in a similar way, although you may wonder why we need to. There aren't really multiple types of strings the way there are integers, but optionality becomes an issue when working with Core Data.

Core Data gives you a choice about whether or not a value can be optional. Confusingly, this doesn't line up perfectly with an optional value in Swift. Instead, a value that isn't marked optional in Core Data is still treated as optional in Swift until the point that the data is saved. At that point, the attribute must be filled in (have a value), but up until then it can be nil. 😬

The result is that when you try to use string attributes in places where Swift is expecting a non-optional String, the compiler is going to complain that you haven't unwrapped.

Code snippet of string-related computed properties

These two lines automatically unwrap the values of author and category when referencing them. This is really only a convenience, but one that I find helpful because I don't think of these as "optional" values.

You may note that these are read-only computed properties. The author and category properties can be referenced directly when new values need to be written. You could easily add a setter to these if you prefer.

I actually haven't decided how categories are going to work for this app yet, so routineCategory will very likely change in the future. I'm thinking that the categories will be limited (users can't just type anything at all), but I also don't want them to be enumerated in the code because I don't want someone who is a version behind to have to miss out on a category just because they didn't download the version of the app in which I typed that string.

Enums

The other type of string conversion I want to point out is something I've already mentioned - enums. In the Routine extension, I defined an enum at the top called Origin with cases user and downloaded. Obviously these could very easily just be strings, but then we'd be using magic strings which is not a Swifty thing to do. Instead, we can just use another computed property to convert them easily.

Code snippet of routineOrigin computed property

As you can see, it's super simple to do this conversion as well, to the point that the benefit of using magic strings is pretty much nullified. This method returns a valid value in all instances (defaulting to 'user'). Since the compiler can type-check whenever you write to this value in your code, it's impossible to write an invalid value to this field (although, note that it IS still possible to write an invalid value directly to the origin attribute directly if you really wanted to). And this method also removes the need for developers to remember the valid options because Xcode will simply suggest them as you type.

Relationship arrays

Last but not least, I want to talk about relationships in Core Data and how we can extend our classes to make them iterable.

Core Data very helpfully provides us with a property on its generated classes that links us to all related objects when we specify a "to-many" relationship. In this app, Routine has a to-many relationship with Step because each Routine needs one or more Steps to define what it will do. (The inverse relationship from a given Step object is a to-one relationship back to the Routine that owns it.)

In the model editor, I've named the attribute that defines this property as steps. In Swift, the property in a Routine object, then, is easily accessible as myRoutine.steps. The catch here is that this property is of type NSSet.

There's nothing wrong with an NSSet, of course. But it's not necessarily the friendliest thing to work with in most cases, especially for apps like this where I'll want to iterate over all of the objects to display them in a List or run through them in order when playing a routine later on.

Code snippet of routineSteps computed property

These few lines of code create a shortcut that automatically transforms the NSSet into a Swift array of type [Step]. You could modify this very easily to make a Swift Set instead if that's what you need, or you could change the sorting closure to sort by any other attribute depending on what you've chosen to store in your model and how you will want to arrange the related items. You could even change this to a method that takes an argument to select a particular attribute to sort on, or make several computed properties with slightly differing names to do the same. Whichever way you go, this property/method will prove to be super helpful!

.example

The final computed property is something else I picked up from Hacking With Swift - an example. static var example: Routine defines a class property that can be used to generate an example object for SwiftUI previews, which is a lot nicer than creating one in any preview view that needs it. Just call Routine.example instead.


That's it for today! I hope you've enjoyed this discussion of Core Data. Let me know in the comments if you have any questions or anything to add. See you next time!

Discussion

pic
Editor guide