In this tutorial, we use the latest version of Xcode (12.4) and macOS Big Sur (11.2.1) for the moment of writing.
In this tutorial, we will use the app developed in the Complex SwiftUI App Tutorial. Part 1. Designing Model tutorial, so I recommend going through it. Otherwise, if you do not want to do this, you can simply download the starter project here.
What We Will Do Today
Today, we will create a view that will present a grid of your goals and another one to add new ones. We will make our code clean and testable and make sure the grid of goals gets updated, when a new goal is added. With time, we will add additional logic layers, views and learn how to make a (relatively) complex app with SwiftUI.
Getting Started
First, make sure you have your starter app ready. It should contain:
- Core Data model file,
- Two files containing Core Data entities
TLGoal
andTLGoalRecord
, ContentView
showing a simple list of goals stored in our temporary persistence for previews.
If you build and run this app, you will not see any data there, so we need to implement adding new goals. But before doing so, let’s first create a nicely looking view showing our goals using LazyVGrid
.
As I mentioned in the previous part, we are going to use a controversial approach when the view will retrieve a list of goals directly from Core Data using
@FetchRequest
, while the rest of logic will be in a viewModel. This is unusual, but we are trying to use some useful SwiftUI stuff, such as this property wrapper.
MyGoalsItemView
First, create a new SwiftUI file called MyGoalsItemView
. Make sure both targets (iOS and macOS) are selected. We will not check if it works in macOS at first and focus on the iOS implementation, but at some point we will add macOS support. Add the following to the new file:
- We add the
goal
value, - We create a
VStack
in the body and put twoText
elements there. First one will contain the goal’sicon
, add a modifier to set its font size to 60. The secondText
will showtitle
with three modifiers: 1) we set the font’s color asprimary
meaning it will be black for the Light Mode and white for the Dark one, 2) we setlineLimit
to 2, 3) we setminimumScaleFactor
to 0.5, so if the text is too long, it will get up to 2 times smaller, - We add modifiers to the
VStack
to stretch horizontally and vertically as much as possible (to have an equal width and height for each item), set aspect ratio, so our items are rectangular, add padding, set the background color, corner radius and apply a shadow to make the view look nice, - We add a static variable for a
TLGoal
in thePreviewProvider
, - We pass the static variable into our view for the preview,
- We add a modifier to present our view in a fixed size layout, 160x160 pixels.
Grid View
Now, let’s create a GridView we will use MyGoalsItemView
in. Go to ContentView.swift
and first of all, rename ContentView
to MyGoalsView
, as it makes more sense than ContentView
.
To rename your struct (class, method or whatever) everywhere, simply hold Command key and click on the struct name
ContentView
and then click Rename. Rename your struct toMyGoalsView
, then click OK. Now, you should see it is renamed. So is the filename of the files it is declared in.
Now, we will make the following changes in the file:
- We add a new variable
columns
that contains an array with just oneGridItem
which is.adaptive
meaning the grid will be filled with items of 100–160px width depending on the size of your device. On iPhone, it will have 3 items in a row while there might be more on iPad, - We add a
ScrollView
withLazyVGrid
inside that presents our goals, each in itsMyGoalsItemView
.
Now, if you check the preview in the Canvas, you will see our view with the grid of goals:
But if we build and run the app, we will see there is nothing there. We need to add a capability to add a new goal, so we can actually start tracking how we achieve our goals and go toward our dreams. But first of all, let’s implement a view, showing there are no goals instead of the grid.
For now, we will simply present a Text
and at some point we might want to create a separate view for this that will have an image and/or a button. But for now, add these changes:
- We added an
if-else
block that presents a simpleText
when there are no goals, - We put all our body in
Group
so we can apply.navigationTitle
to the whole code instead of attaching it to the newText
and theScrollView
, - We also added a
NavigationView
, so our view will have a navigation bar with a title, - We added a title to the navigation bar of the view.
Great job! Now, it’s time to create a view where we will be able to add a new goal to our app.
AddNewGoalViewModel
First, create a viewModel for our AddNewGoalView
. Create a file AddNewGoalViewModel.swift
:
For now, it contains only title
and icon
for a goal. Also, it has the save
method that we will implement later.
Also, it uses GoalIcon.all
, while GoalIcon
does not exist yet. Let’s fix it. Create a new file GoalIcon.swift
. We will make it simple for now but might need to refactor later:
Great. Now, we have our viewModel and ready to create a view.
AddNewGoalView
Create a new SwiftUI View
file, call it AddNewGoalView.swift
. This view will contain a TextField
to set the goal’s title and a selector to choose an icon.
First, add viewModel
to the view and add a TextField
to the body
:
Note that we put the TextField
in a Form
. Now, we will add another Section
to the Form
with a Picker
for icons:
Last but not least, we will add a top bar to the view with two buttons, Cancel
and Create
:
- We add an
@Environment
variablepresentationMode
, so we can dismiss this view, - Embed the
Form
intoVStack
, - Add a
HStack
with twoButtons
.Cancel
will simply dismiss the view, whileCreate
will call thesave()
method in theviewModel
and then dismiss the view.
DataManager
We will need a layer that will manage Core Data entities, because we do not want to expose Core Data in every screen working with our model. We will create DataManager
that, for now, will be able to create a new TLGoal
:
- Import
CoreData
, - Create a protocol
GoalDataManagerProtocol
, - Create a
typealias DataManagerProtocol
. For now, it will be justGoalDataManagerProtocol
, but later, we will add other protocols andDataManagerProtocol
will be a general protocol including all related to theDataManager
, - Create the class,
- Implement a
Singleton
variableshared
, - Inject
PersistenceController
, - Implement
createGoal
: updateposition
for all the existing goals and create a new one in the 0th position.
Note that for now, we use
viewContext
in theDataManager
. It may make sense to create a new background context for our methods in the future, but for now, we are fine with this implementation.
Now, we will add the method to AddNewViewModel
:
- Inject our new
DataManager
, - Use the
createGoal
method insave()
.
Now, we need to add a button to the main view to present AddNewGoalView
, let’s do this. Open MyGoalsView.swift
and add the following:
- Add a new
@State
variableshowingAddNew
, - Add a
.sheet
modifier that will presentAddNewGoalView()
whenshowingAddNew
istrue
, - Add
addNewButton
that will setshowingAddNew
totrue
, - Add this button to the view using the
navigationBarItems
modifier.
Now, build and run the app. Then try to create a new goal. Notice that, once created, the goal appears in the main view because our @FetchRequest
reacts n the change in the context.
What’s Next
Congratulations! We have created the main view in our app, and now we can add goals for us to stick to and obtain (or get rid of) new habits! We used SwiftUI and Combine, the MVVM pattern and used Protocols to make your code clean, readable and testable. In the next part we will add a ContextMenu
to the grid items, so we can edit and delete our goals. Also, we will add a view to edit our goals and finally, we will make our goals tappable, so we can mark them as completed for a day to start recording our progress on how we become better with time.
The complete code of the app is available here.
The next tutorial is available here.
This tutorial is the 2nd part of the Complex SwiftUI App Tutorial. To check the other parts, use the following links: