iOS App Development
Complex SwiftUI App Tutorial. Part 1. Designing Model
In this series of tutorials, we will create a complex app using SwiftUI, Combine, Core Data, MVVM pattern, and Protocols.
In this tutorial, we use the latest version of Xcode (12.4) and macOS Big Sur (11.2) for the moment of writing.
There is no shortage of SwiftUI tutorials nowadays, so everyone can find something that works most for them. I created such a series of tutorials too.
But those tutorials are mostly for beginners and do not cover much of the additional features that SwiftUI or Combine have. Nor do they have complex architecture or logic.
In this series, we will create a complex app with several views, several Core Data entities, and a lot of under-the-hood things. It will take a while, and of course, it won’t be a complete ready-to-publish app, but it’ll be something a bit more advanced.
So, in an attempt to make this really long series of tutorials as short as I possibly can, I will expect you have some basic understanding of SwiftUI, Combine, and the MVVM architectural pattern, and I won’t focus on simple things. However, if you find something difficult, feel free to let me know, so I can help you go through the series.
So, let’s begin.
What We Will Do Today
To make our journey a bit more enjoyable at the beginning, we won’t dive into architecting our business logic deeply, but we’ll create the main model for our app in Core Data and present it in a simple way in a SwiftUI view to make sure it works. As a result, by the end of this first part, we will have a foundation for our app and will focus on creating the main view and implementing basic actions so we can understand what exactly we’re going to make by the end of the series. After that, we will focus on adding logic, diving into the implementation of more complex parts of the app, and deciding what features our app will have.
Maybe it’s not the best approach, and in a real project you should firstly create most of the logic, cover it with tests and then make UI, but it’s not always the case, especially when it comes to tutorials. Anyway, we’ll try to make our process of creating this app as close to real work as possible while implementing both logic and UI simultaneously.
Idea
We all have tried to obtain a new habit or quit a bad one. In many times dedication and consistency work, and many of us may notice that after some magic number of days (21, 30, whatever) it gets easier to stick to your new habit or withstand a desire to get back to the old one you want to get rid of.
The app we are about to start making will be about habits. We’ll make an app to track whether you stick to your goal you’ve decided to stick to or quit a bad habit. We’ll record our progress each day and present some kind of statistics.
I’ll call it Daily Goals but you can choose any name for it. So let’s start.
Taking Off 🛫
Open Xcode, select Create a new Xcode project, select the Multiplatform tab and click on App and then click Next. Type your app’s name (Daily Goals or any name you like), choose Team and Organization Identifier, and make sure you checked Use Core Data (as well as Host in CloudKit), as we’ll use Core Data in our app. You can uncheck Include Tests, as we won’t cover them in this tutorial, although I believe it’s important to cover as much code as you can with tests in a real app.
Press Next, choose where you’d like to store your project’s files, and click Create.
Now, you should see a project with one simple SwiftUI view, PersistenceController, and folders (aka Groups) Shared, iOS, macOS.
Model
The first step is to create a Core Data model for our goal’s entity and get rid of Item
created by default. Select your xcdatamodeld
file.
Click on Add Entity at the bottom of the screen and you should see a new entity called Entity
. Rename it to TLGoal
. TL
stands for Tutorial but you can choose your own prefix.
I recommend using a prefix for your entities, so your naming doesn’t interfere with the naming of the Swift (or SwiftUI) classes, structs, property wrappers, etc. For instance, if we’re going to put our goals into lists, it won’t be a good approach to call an entity
List
because SwiftUI already has the viewList
. As a result, you’ll see weird compiler errors, won’t be able to easily use your entities and make your life a bit more difficult. While a prefix ensures, you’ll unlikely have such an issue.
Every goal will have an id, icon (we’ll use emoji for this tutorial), title, sorting index, date it’s created on, the date it’s updated last time on, flag showing if it’s deleted (we’ll keep our goals deleted for several days before actually removing them from the database, so that flag will come in handy).
Let’s add all the attributes to your new entity:
id
, typeUUID
,icon
, typeString
, non-optional, default value: empty string (you can set these settings in the Data Model inspector in the right panel,title
, typeString
, non-optional, default value: empty string,position
, typeInteger 16
, non-optional, default value: 0,addedOn
, typeDate
, non-optional, default value: any date in the past (you may leave the one added by default),modifiedOn
, typeDate
, non-optional, default value: the same as above,isRemoved
,Boolean
, non-optional, default value:NO
Also, let’s create an entity for storing records for our goals, so we may collect statistics on how the user performs over time. We’ll call it TLGoalRecord
, it’ll have just one attribute:
date
, typeDate
, non-optional, default value: any date in the past.
Also, add a relationship to this entity. To do this, click on the plus button in the Relationships group, name it goal
, destination will be TLGoal
. In the Data Model inspector, make sure it’s Delete Rule is Nullify
, Type is To one
.
Now, select TLGoal
and add a relationship to it. We’ll call it records
, destination will be TLGoalRecord
, Inverse will be goal
. Set Delete Rule to Cascade
, Type to To Many
.
Great! Also, let’s delete the automatically created Item
entity. Select it and press Delete on your keyboard.
As a final step for our Core Data model, let’s manually create Swift classes for our entities. First, select TLGoal
, then change Codegen to Manual/None
in the Data Model inspector. And then, do the same for TLGoalRecord
.
Boring database-related stuff is almost done. But there are a few extra steps required. First, select Persistence.swift, and comment or delete all the code related to the old entity Item
in the preview
variable, so it only returns an instance of PersistenceController
that is stored in memory. Do not delete this variable, we’ll need it later, as it’s convenient to create a Core Data model stored in memory for our SwiftUI previews (and for unit tests as well). We’ll add a creation of Goals in here, but later.
Now, open ContentView.swift and comment or delete everything related to Item
there. Basically, you need to add to the ContentView
class body
variable Text("Hello, world!")
and remove everything the compiler shows errors for. Make sure the app builds with no errors, so we won’t be interrupted while working on our app.
Model Files
First, create a new file TLGoal.swift:
- We import
CoreData
, - We create a class
TLGoal
with all the params - We also add an extension with
fetchRequest()
that returns only not removed items sorting them byposition
, - We conform the class to
Identifiable
, so we can use it in SwiftUI views
Here I should point that there are different approaches on how to use Core Data in an app. One way to use it is to encapsulate it into a layer closed to the whole app but one DataManager and use other classes or structs to convert Core Data entities into. In this case, at some point, you may throw away your Core Data layer and replace it with anything else (Realm or anything else).
I used it in my previous series of tutorials.
Another approach (that will be used in this tutorial) is to use Core Data entities themselves and expose a larger part of your Core Data layer, letting the app know you’re using it. It might make your life harder, should you migrate to another persistence, however, you can use all the benefits of Core Data (better performance and memory management, use of @FetchRequest
property wrapper, etc.). If for any reason, you’ll decide to migrate to Realm or something else, you’ll have to rewrite a more significant part of your app. So, learn more about the pros and cons of both approaches to make the best decision for each particular app.
Now, create a file TLGoalRecord.swift:
Here we do the same:
- We import
CoreData
, - Define our class and its properties,
- Add
fetchRequest()
, - Conform to
Identifiable
Preview
Awesome! Now we have our model ready. But before wrapping up and moving to another part, let’s populate our preview
container and show goals in the preview provider for the main view, so we can check if our data model works fine.
Now, create a new file Persistence+Preview.swift, add a new extension to it:
- Import
CoreData
, - Add a
static
method for adding a goal, - Create a method
addPreviewData
to generate goals for our previews
Then, open Persistence.swift and add this method to the variable preview
:
As you can see, we replaced old data with our method on the line 7.
Now, let’s present the temporary goals in our view to make sure they’re added properly. Go to ContentView.swift and do the following:
- Create a
private
variable for goals with theFetchRequest
attribute using ourfetchRequest()
added in TLGoal.swift, - Add a simple
List
with all the goals we have
Now, run the preview on the Canvas and you should see our goals:
Note: even though we’re going to use MVVM in our app, I decided to try a controversial approach with the use of @FetchRequest
. It might not work eventually, so if it happens we refactor that, but the current idea is to use this wrapper and put the rest of our logic inside a viewModel. We’ll see if that works.
What’s Next
Congratulations! We’ve started our project with SwiftUI and Core Data. Even though our app doesn’t show any info, we’ve set up a foundation for our future features. In the next part, we’ll create a view and viewModel for our list of goals and implement adding a new TLGoalRecord
on tap. We’ll make our code clean, we’ll use Dependency Injection to make our app testable and make sure we hide behind protocols as much as possible to encapsulate all the layers.
The complete code of the app is available here.
The next part is available here.
This tutorial is the 1st part of the series of the creation of a complex SwiftUI app. Links to the next parts will appear when the parts are published.