Composing the interface

Composing the UI of the TasksView 👨‍🎨

Before we learn how to use our CoreData data model to save and retrieve our To-Do items, we create our app’s interface. Let’s start with the view listing every pending task. We create a new SwiftUI View file named “TasksView.swift” for this. At this point, you can delete the ContentView.swift file. Consequently, we must initiate the TasksView in our To_DoApp struct instead.

import SwiftUI

@main
struct To_DoApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            TasksView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

For testing purposes, we generate some dummy data. Thus, insert an array into your TasksView assigned to some sample task titles.

struct TasksView: View {
    
    var sampleTasks = [
        "Task One", "Task Two", "Task Three"
    ]
    
    var body: some View {
        Text("Hello, World!")
    }
}

For displaying the different tasks, we use a List. We use the GroupedListStyle for this List.

var body: some View {
    List {
        
    }
}
    .listStyle(GroupedListStyle())

Our List should contain: First, dynamic rows for displaying the different tasks that can change during the app’s runtime. Second, two static rows that are always the same – one for entering a new task and one for navigating to the view containing tasks that are already done.

To combine static and dynamic rows, you can insert a ForEach loop into the List. Inside this loop, we will create our dynamic task rows. Below this loop, we will insert our two static rows.

List {
    ForEach {
        //To-Do's (dynamic row(s))
    }
    //Row for adding a new task (static row)
    //Row for navigating to the view containing accomplished tasks (static row)
}

Let’s start with the ForEach loop. A ForEach loop cycles through a given sequence and creates one view for every element inside that sequence. 

We use our sampleTasks array for creating one row for each String inside it. To do this, we write:

ForEach(sampleTasks, id: \.self) { item in
    Text(item)
}

\.self tells the ForEach loop that each item of our sampleTasks is uniquely identified using itself as its value. For example, the identifier of the first sampleTask is “Task One“. Note that this only works safely when all elements inside a sequence are unique.

The created rows should also show a circle icon on their right sides:

ForEach(sampleTasks, id: \.self) { item in
    HStack {
        Text(item)
        Spacer()
        Image(systemName: "circle")
            .imageScale(.large)
            .foregroundColor(.gray)
    }
}

When the user taps on this icon, we want the tasks to be marked as completed. We will implement this logic in a moment. For now, wrap the Image view into a Button and provide it with a dummy print statement.

Button(action: {
    print("Task done.")
}){
    Image(systemName: "circle")
        .imageScale(.large)
        .foregroundColor(.gray)
}

Each row in our app should have a certain height. Let’s create a property for this. Since we will also use it outside the TasksView struct, make sure you place it outside it.

var rowHeight: CGFloat = 50

We can use this property to determine the height of our dynamic rows.

ForEach(sampleTasks, id: \.self) { item in
    //...
}
    .frame(height: rowHeight)

Great! We finished composing the rows for our to-do’s. Your preview should look like this now:

Next, we are going to create the first of our static rows. This will be the row where the user can create a new task.

We will use a TextField for this, so create a State property for holding the user’s input:

@State var newTaskTitle = ""

Below the ForEach loop, we can now insert a TextField. When the user taps the return key on his keyboard after entering the new task’s title, we want to add a new To-Do item. We do this later, so insert a dummy statement for now.

TextField("Add task...", text: $newTaskTitle)
    .onSubmit {
        print("New task title entered.")
    }

This row should also contain a Button for adding a new task. This Button should be disabled as long as the user hasn’t entered anything into the TextField.

HStack {
    TextField("Add task...", text: $newTaskTitle)
        .onSubmit {
            print("New task title entered.")
        }
    Button(action: {
        print("Save task")
    }) {
        Image(systemName: "plus")
            .imageScale(.large)
    }
        .disabled(newTaskTitle.isEmpty ? true : false)
}

Let’s make this row higher as well.

HStack {
    //...
}
    .frame(height: rowHeight)

Great! The only thing left now is the row, which the user can tap on to navigate to the view showing all tasks already completed. We will create the corresponding NavigationLink later. For now, just insert a Text view below the HStack inside the List:

Text("Tasks done")
    .frame(height: rowHeight)

We already finished composing the interface of our TasksView. We only need to add a navigation bar to it. To do this, wrap the List into a NavigationView and apply the .navigationTitle modifier to it.

var body: some View {
        NavigationView {
            List {
                //...
            }
                .listStyle(GroupedListStyle())
                .navigationTitle("To-Do")
        }
    }

Awesome, we finished setting up the first main view of our To-Do app!

This is how your preview should look like now:

3 replies on “Composing the interface”

Leave a Reply

Your email address will not be published. Required fields are marked *