29 Apr 2021
Just as we can prepare pasta from scratch and have it then ready to be cooked, we can prepare our data to be available for use by our app!
Okay, I admit that was a terrible analogy - I think I’m just hungry… also note that this is quite a long and technical blog post - primarily because it took me so long to figure all of this out!
There are many ways you can store data for an iOS app. You can store it in a local database (which resides on the actual iPhone), in a flat file (like our JSON file), or you can store it externally in a database on some server somewhere. While I have the data currently in a JSON file, I don’t want to load that whole file every time I start the app - it is really big! In addition, I want to store values that can change, like inputted characters in a crossword or a list of completed levels.
I decided to use one of Apple’s built-in options to store all this data for a few reasons: (1) better performance using a built-in database than using one hosted on a server somewhere, (2) good documentation provided by Apple on how to set it up since it is commonly used, (3) easy-to-setup iCloud syncing between devices, (4) it is completely free to use, and (5) as a beginner, I just wanted to use the most barebones approach so that I can understand iOS internal storage systems better.
In particular, I am using iOS’s built-in data storage solution called Core Data. This is the framework Apple uses themselves when they need to allow the user to store data in their apps (the Photos app is one prominent example). Core Data uses SQLite behind the scenes, but also does much more on top of that. You can save property lists, sync the database between Apple devices (e.g. an iPhone and and iPad) using a service called CloudKit, and easily edit the database structure from an interface in XCode (the program used to create iOS apps).
I had also learned a good bit about how to use Core Data in my iOS development course, so this was a perfect chance to try it out!
Before importing any data into our app, I first needed to create the barebones of the app! When you start a new iOS project in XCode, it automatically generates the following code so you have a functional barebones app right out of the gate!
This first file creates the app and says we have one view, the ContentView (currently the only view in our app).
This next file creates said ContentView. The top section is where we would begin designing our UI and anything you see on the screen (I just added some text saying “Test” so far). Eventually we will have many view files (one for the menu screen, one for the title screen, etc.) and they will all be hooked up to one another so you can navigate between them. The bottom section is just for XCode to create a preview, like in the image below.
Now that we have our (super basic) app up and running, we can write some code to load puzzle data into our app!
If you remember back to the last blog post, we discussed data models, like the one I created for this app:
Well, now it is time to tell our app about this data model so it can create a database for us. Then we can tell the app to load the big JSON file we created with all our puzzle data and have the app store that into the database according to the data model.
To tell our app about our data model, we have to create a Core Data Context object and a DataStore object. This Context object allows us to interact with a database built based on our data model, represented by our DataStore.
Within my DataStore, I needed two individual Stores, one for the local data we save only on the device, and the other for data both saved locally and to the cloud (for syncing across devices).
It took me multiple days to figure out how to do this, but in the end I figured it out. The following is the main bit of code I wrote to solve this problem.
Walking through this, line by line:
This guy just ensures that we create a single (i.e. static) DataStore object that gets shared by any other entities in our app that want to interact with it, i.e. we can just do DataStore.shared wherever we need to use it in our app.
Here, we are creating our persistentContainer, which you can just think of as the actual database object. I give it a name and also get its location in the file system for the next step.
In the first part, I’m specifying the location in the filesystem for the local part of the database, and letting the app know this is for local data only. In the second part, I do the same for the cloud-based store.
And here I just give the cloud-based store a link to an online version of the database to sync any data to. I had to set up this link and database beforehand using the online CloudKit Dashboard.
Finally, I instruct the container to try and load the database when the DataStore object is first initialized (which occurs in the very first line of the DataStore struct block). It prints out an error message if needed.
That’s pretty much it! There were some other bits of code I had to write to do things like save data to the database or wipe the database (I made a lot of mistakes when I was first trying to save the puzzle data!). You can see the full file here if interested.
So even though I created a data model already in the app, I still need to specify exactly what data the app should expect when it reads the JSON file with all our puzzle data. To do this, I created a Swift “model” file called KameKurosuPuzzleData.swift:
Note that this “model” for our JSON file is technically different than our “data model” I alluded to earlier. However, you can see it is very similar - and this is because we intentionally made our JSON file look similar to our “data model” to make it as easy as possible to import all the data!
Specifically, to read a JSON file, Swift requires you to create a hierarchy of structs following the “Decodable” protocol. This ensures that as Swift reads the JSON file, it knows exactly what to expect in terms of how the data is organized.
At this point, we can do something like the following to read all this data into the app (but not yet into the database!):
The first line initializes the decoder we can use to read the JSON file. Note that in the next line we feed it the filepath to our JSON data file. We then read the data from the file, printing out a useful error message in case we run into any errors or bugs. The KameKurosuPuzzleData.self argument to decoder.decode() points the decoder to our model file which we created above.
Now that we have all this data loaded into the jsonData variable, above, we can figure out how to insert it all into the database!
The next thing I did was create a function called loadJsonDataIntoCoreData to take our jsonData and insert it into our database. It’s quite long, but here is the finished result:
I’ve omitted the first two helper functions for the sake of brevity. At a high level, the function basically:
Note that the Level, Puzzle, Word, and Cell objects relate directly back to our data model at the beginning of the blog post. These structs are created automatically for us to use by XCode.
I wrote a very similar (but much shorter) function as well for the cloud-based data (creating and saving UserInfo and CompletedPuzzle objects to the cloud store).
And thats it! We can now use these functions to take all the data in our JSON file and write it into our core data database!
The final step is to make sure that all of this JSON data is only loaded the very first time the user loads up the app. Every subsequent time, we will already have the data in the database so don’t need to transfer it!
I started by updating my main file that starts up the app. I added some code to let me know in the debugger (text output only the developer can see) whether the app is being entered, exited, etc. I also wrote a bit here to make sure to save any data that has recently changed to the database if the user leaves the app.
I also wrote some code near the top of this file to grab our DataStore’s context (so we can interact with it from this file) and load everything if the DataStore is empty.
Specifically, I wrote a method called CoreDataInitializer.initializeCoreDataIfEmpty() which is shown below:
I also moved all the functions I wrote previously to transfer the data into the database from the JSON file into this struct.
I basically just check to see if there are any Level entries in the local store and any CompletedPuzzle entries in the cloud store. If not, I assume it is empty and use the functions written previously to load the data in.
We first had to choose a good method to persist our data inside our app. Because I wanted something that was relatively easy to set up, I picked Core Data, which Apple themselves use for many of their built-in apps. I also decided to use CloudKit to allow for syncing of some data (like which levels have been completed) between devices.
I then created a barebones app that basically does nothing except show some text on the screen.
Once I had this minimal app up and running, I wrote (and rewrote!) a bunch of code to set up the database and transfer all of our puzzle data into it the first time the user opens up the app.
And that’s it! As you can see, this was quite a bit of work and took me a very long time. But I’m happy this step is finally done! Now that all our data is present in our app, we can work on the UI and logic of how our app behaves!