iOS App Development

Complex SwiftUI App Tutorial. Part 2. Main View

In this tutorial, we will add a view and its viewModel for the main screen of our SwiftUI app. We will use Combine, Core Data, MVVM pattern and Protocols.

Alex Zarr

--

Screenshot provided by the author

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 and TLGoalRecord,
  • 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:

  1. We add the goal value,
  2. We create a VStack in the body and put two Text elements there. First one will contain the goal’s icon, add a modifier to set its font size to 60. The second Text will show title with three modifiers: 1) we set the font’s color as primary meaning it will be black for the Light Mode and white for the Dark one, 2) we set lineLimit to 2, 3) we set minimumScaleFactor to 0.5, so if the text is too long, it will get up to 2 times smaller,
  3. 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,
  4. We add a static variable for a TLGoal in the PreviewProvider,
  5. We pass the static variable into our view for the preview,
  6. 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 to MyGoalsView, 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 one GridItem 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 with LazyVGrid inside that presents our goals, each in its MyGoalsItemView.

Now, if you check the preview in the Canvas, you will see our view with the grid of goals:

Screenshot provided by the author

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:

  1. We added an if-else block that presents a simple Text when there are no goals,
  2. We put all our body in Group so we can apply .navigationTitle to the whole code instead of attaching it to the new Text and the ScrollView,
  3. We also added a NavigationView, so our view will have a navigation bar with a title,
  4. 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 variable presentationMode, so we can dismiss this view,
  • Embed the Form into VStack,
  • Add a HStack with two Buttons. Cancel will simply dismiss the view, while Create will call the save() method in the viewModel 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 just GoalDataManagerProtocol, but later, we will add other protocols and DataManagerProtocol will be a general protocol including all related to the DataManager,
  • Create the class,
  • Implement a Singleton variable shared,
  • Inject PersistenceController,
  • Implement createGoal: update position for all the existing goals and create a new one in the 0th position.

Note that for now, we use viewContext in the DataManager. 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 in save().

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:

  1. Add a new @State variable showingAddNew,
  2. Add a .sheet modifier that will present AddNewGoalView() when showingAddNew is true,
  3. Add addNewButton that will set showingAddNew to true,
  4. Add this button to the view using thenavigationBarItems 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:

  1. Complex SwiftUI App Tutorial. Part 1. Designing Model.
  2. Complex SwiftUI App Tutorial. Part 2. Main View. (this tutorial)
  3. Complex SwiftUI App Tutorial. Part 3. Actions.

--

--