WOODY'S
FINDINGS

Scripting with SwiftUI (III) - Forms

Tutorial

13 June 2021

This tutorial is the third one of the series "Scripting with SwiftUI". This series goal is to use SwiftUI in Swift scripts that can be easily shared. As this use case is more common for Mac admins, those tutorials are addressed to this audience. Noneless a Swift developer could find interesting techniques in those tutorials.
If you have not already done it, I advice you to follow the first two tutorials Coding a color picker view and Scripts provider which explain SwiftUI knowledge required for this tutorial.

Exposition

In this tutorial, we’re going to implement a simple form to ask some (very basic) information to the user. Then, we’ll see how to export those information with the Codable protocol in a JSON file. This can be useful to send this file to a remote server or to store it for a later use. The last part is optional and deals with data validation and how to show warnings to the user.

Here are some screenshots.

The equipment boxes list appears only when the box "I want to update my equipment" is checked.

In the last optional part, I'll show how to implement validation on the form. When the "Send" button is clicked and one of the text field is empty, a cross icon appears, and a popover is shown when hovering the cross.


Quick project overview

You can find the materials for the tutorial here. Open the starter project, you should see some code already present and which is similar to the one written in the previous tutorial. Here is a quick overview:



Building the form

The models

With the screenshots above, it’s possible to guess how the properties we are going to export will be stored. What we will need:

How are we going to group those properties? If you read the previous tutorial "Scripting with SwiftUI - Script provider", you might already have an idea. If we were to only store this information, a plain struct would be ok. Here however, the modifications of those properties should have an impact on the view. For instance, checking the box "I wish to update my equipment" should show the equipment list. Also, if the properties were retrieved from a file (as the one we are going to export), the properties values should be reflected in the UI.
In short, we are going to use an ObservableObject with @Published properties.

Quick reminder
  • ObservableObject is a protocol. If you do not know what a protocol is, you might want to read my explanation in the previous tutorial.
  • An ObservableObject is a class whose @Published properties changes can be observed. @Published is part of Combine, and SwiftUI automatically handles that for us. By implementing one, we get a SwiftUI View updating when any object’s published properties is modified.

Let’s start with the easy properties: first and last name. The model will be named Infos and will be a class conforming to ObservableObject. You can add it right below the MARK: - Models delimiter.

// MARK: - Models

final class Infos: ObservableObject {
    @Published var firstName = ""
    @Published var lastName = ""
}

We could use optional properties (and this would be clearer), but we are going to need them in a TextField view which takes a non-optional String (wrapped in a Binding).

For the "Department" list, an enum is a good choice for this tutorial since it will store a finite amount of values that we will be able to browse by adding a conformance to CaseIterable. Here it is.

enum Department: String, CaseIterable {
    case sales = "Sales"
    case it = "IT"
    case marketing = "Marketing"
    case communication = "Communication"
}

As mentioned, the conformance to CaseIterable allows to call the property Department.allCases which will yield the following array.
[Department.sales, Department.it, Department.marketing, Department.communication]
As the enum declare String raw values, a simple map can transform the array of Department into an array of String:

Department.allCases.map(\.rawValue)
// prints ["Sales", "IT", "Marketing", "Communication"]

A department property can now be added to the Infos class. The default value is arbitrary set to Department.sales and will appear as the default option in the department list control.

final class Infos: ObservableObject {

    @Published var firstName = ""
    @Published var lastName = ""
    @Published var department = Department.sales
}

For the equipment part, we will first add an equipmentUpdate property to specify whether the equipment checkboxes should appear, then an equipment property will store which equipment box is checked. To do so, the Equipment struct is implemented.

struct Equipment {
    var mac = false
    var headset = false
    var mouse = false
    varkeyboard = false
}

Now the Infos class can be completed.

final class Infos: ObservableObject {

    @Published var firstName = ""
    @Published var lastName = ""
    @Published var department = Department.sales
    @Published var equipmentUpdate = false
    @Published var equipment = Equipment()
}

By default, the equipmentUpdate property is false, meaning that the box "I wish to update my equipment" will be unchecked. The same goes for the equipment list boxes.

Now, we are ready to build the interface. Let’s craft a bit!


Building the UI

As usual in those tutorials, the MainView will be the root view, holding all the other ones. Thus, it makes sense to make it the owner of the Infos property that the sub-views will share and modify. So, before coding the form rows, please add the following to the MainView, above its body property.

struct MainView: View {

    @StateObject var infos = Infos()

        var body: some View {
            // ...
    }
}

Wait… what is @StateObject now? As if we did not have to handle several property wrappers in SwiftUI 🙄
Yes, it’s another property wrapper to remember, but it’s for our own good, trust me! In the previous tutorial, we used @EnvironmentObject which is more convenient. It allows any subview to access to the instance passed to one of its parent with the environmentObject modifier. It would be fine to use it for this tutorial but I wanted to show another way. By declaring a @StateObject in the MainView, we specify that the MainView owns the infos instance, and is responsible for creating it. Then, subviews that need to access to the infos instance will declare an @ObservedObject, which will be passed in the init. This is more restrictive because it forces us to pass it every time it’s needed. But it’s also clearer because we know what view needs it.

If we were to implement a deeper view hierarchy with some views needing to access to infos, and some others not, an @EnvironmentObject would be a better choice than @ObservedObject. Anyway, the MainView would still have to declare a @StateObject since it’s the one creating it.

After this SwiftUI theory, it’s time to code the form rows!


Name rows

Let’s start with the first two text fields. They are exactly similar, so we will implement only one View that will be reused. Here, they are two views that are stacked horizontally. Thus, we are going to put them inside in a HStack this time. The view on the left simply displays text, so a Text view should be enough. The right is new though really simple. It’s a TextField with a placeholder. Thanks to SwiftUI, implementing those views is easier than with AppKit.

Here is how the Name row looks like. You can add it below the MARK: - View delimiter.

struct NameRow: View {
    // 1
    @Binding var value: String
    let label: String

    var body: some View {
        HStack {
            // 2
            Text("\(label):")
                .frame(minWidth: labelsWidth, alignment: .trailing)
            // 3
            TextField(label, text: $value)
        }
    }
}
  1. The value of the text field is declared as a Binding. If you remember the last tutorial, this indicates that the view should receive a link to an initial State property. Here, the property will be $infos.firstName or $infos.lastName. When dealing with a @StateObject, one of its Published property can be passed as a Binding. The label property is used for the Text view.
  2. The Text view could adapt its width to give more or less space to the TextField. As it’s nicer when everything is aligned, we will give all labels on the left a minimum width and make them aligned on the trailing side (right for left to right language). The labelsWidth is a constant declared in the top of the file and equals a quarter of the window’s width.
  3. The TextField is really straightforward. It takes a placeholder as a StringProtocol (like a String but more generic), and a binding to a value that will be the one entered by the user.

To use this new NameRow, please replace the MainView.body property with the following.

Form {    // 1
    VStack { // 2
        // 3
        NameRow(
            value: $infos.firstName,
            label: "First name")
        NameRow(
            value: $infos.lastName,
            label: "Last name")
        }
    }
}
  1. SwiftUI offers a very nice way to declare views that acts as a form. Using the Form struct to wrap our form rows, we get many features for free , like Section to declare sections or visual elements like grouped table views in iOS. I do not find this to be on macOS in this tutorial, but as SwiftUI is cross-platform, it’s good to be able to export this view to iOS or iPadOS one day.
  2. As you might have guessed already, the form rows will be packed in a VStack. As we use a Form, this is not required, since it already accept a list of views. But it’s the simplest way I found to properly align controls in the « macOS  way »
  3. The two NameRows are declared here, and receive the infos instance first and last name properties. Thus, when the user will enter some text in the text fields, the properties that will be modified will be infos.firstName and infos.lastName. Great!

You can already run the project to see the window appear with the two name rows.

And we’re just warming up!


Department row

The department row should display a list of the possible departments the user can choose from. If you remember the first tutorial, we used something similar to choose a color. In SwiftUI, this is called a Picker. It will take a value to update (so here a Department), a label to be displayed next to the selection, and a list of values that can be selected (here, our Department cases).

Here is the implementation.

struct DepartmentRow: View {
    // 1
    @Binding var department: Department

    var body: some View {
        Picker(
            selection: $department, // 2
            label:
                Text("Department:") // 3
                    .frame(minWidth: labelsWidth, alignment: .trailing))
        {
            // 4
            ForEach(Department.allCases) { department in
                Text(department.rawValue).tag(department)
            }
        }
    }
}
  1. As for the Name row, the DepartmentRow will receive a Binding to indicate which property of the infos instance has to be used for the picker value. We will pass infos.$department when declaring it in the MainView.
  2. The picker selection parameter takes a binding, so here the binding $department is provided.
  3. The picker label is set to have a minimum width, as for the NameRow label, so that labels are properly aligned.
  4. To specify the values in the picker list, a ForEach structure is used. We provide it the Department.allCases list. Then for each department, a simple Text view is used to display the department name. The tag modifier is use to uniquely identify the Text view and to associate it to the proper Departmentvalue to update the picker value (the department property).

If everything went fine, the compiler should complain (but its fine 😉). The problem is that a ForEach structure works with Identifiable types. In the first tutorial, we used integers to feed the ForEach, which already conforms to Identifiable. But here, we have to do it ourselves. Fortunately, it’s quite easy. If we look at the Identifiable requirements, there is only one:

protocol Identifiable {
	associatedtype ID: Hashable
	var id: ID { get }
}

To make the Department type Identifiable, we have to add the Identifiable declaration and a property named id which has a type conforming to Hashable. This might seem complex at first, but it’s easier done than said. Here is how.

enum Department: String, CaseIterable, Identifiable {
    case sales = "Sales"
    // other cases ...

    var id: String { rawValue }
}

Now the compiler should be quiet and we can add the new department row to the MainView right after the second NameRow.

Form {
	VStack {
		// other views
		NameRow(
			value: $infos.lastName,
			label: "Last name")
		DepartmentRow(department: $infos.department)
	}
}

You can now run and select the department.


Equipment row

The equipment row will display the checkbox to select some equipment, as well as the equipment items list.
To do so, it will need to access the infos.equipmentUpdate and the infos.equipment properties. If it’s possible to use Bindings again, it’s a good opportunity to use an ObservableObject.

The views will be inside a VStack. The equipment checkbox is quite simple to implement, whereas the equipment checkboxes will require a bit more work.

struct EquipmentRow: View {

    // 1
    @ObservedObject var infos: Infos

    var body: some View {
        VStack(spacing: 5) { // 2
            HStack { // 3
                Text("Equipment:")
                    .frame(minWidth: labelsWidth, alignment: .trailing)
                Toggle(isOn: $infos.equipmentUpdate) {
                    Text("I wish to update my equipment")
                }
                Spacer()
            }
            // 4
            if infos.equipmentUpdate {
                // to be filled
            }
        }
    }
}
  1. The infos property is an@ObservedObject and will be provided by the MainView.
  2. The vertical stack holding the rows has a spacing of 5 to ventilate the layout.
  3. The checkbox to choose to update the equipment is declared. It’s a HStack that displays the label on the left, a Toggle (the checkbox on macOS) with a custom label, and a spacer to push the toggle on the left. The toggle will update the infos.equipmentUpdate flag.
  4. It’s possible to use branchements in SwiftUI. Even if it’s limited compared to Swift (it’s not the same if), it is quite powerful yet. It allows us to show the equipment checkboxes list only when the infos.equipment update is set to true. This is interesting to show the list only when the user asks for it.

When to prefix with $?
You might have noticed that the equipmentUpdate binding is not prefixed with $ and it's rather the infos object that is. It does not seem intuitive, although there is a reason to explain that. Prefixing equipmentUpdate with $ specifies that we want to access the projected value of the @Published property Infos.equipmentUpdate. That is, a Published value. But this is not a Binding. Thus, it cannot be passed to the Toggle initializer. The solution provided by Apple is to rather prefix the observed object with $ to access the projected value of @ObservableObject. Which is, to quote the doc: « A wrapper of the underlying observable object that can create bindings to its properties using dynamic member lookup. ». So the value returned by $infos allows us to specify a property for which we want to get a Binding. For instance $infos.equipmentUpdate to get a Binding for the equipmentUpdate property.

To display a checkbox with the name of the equipment the usr can select, we will implement a new view called NewEquipmentRow. As mentioned in the previous tutorial, SwiftUI encourages views composition. As the view to display the equipment makes 15 lines it’s a good idea to move it away from the EquipmentRow.

The NewEquipmentRow will display only a checkbox with a label on the right to indicate the equipment name. Also, it will require a Binding to know if it should be checked. The binding will be provided by the EquipmentRow using the infos object.
Here it is.

struct NewEquipmentRow: View {

    // 1
    let text: String
    @Binding var isOn: Bool

    var body: some View {
        HStack {
            // 2
            Text("")
                .frame(minWidth: labelsWidth)
            // 3
            Toggle(isOn: $isOn) {
                Text(text)
            }
            Spacer()
        }
    }
}
  1. The text property will be the label on the right of the checkbox. The binding isOn will be provided by the EquipmentRow view.
  2. To make the checkboxes aligned with the first one (and the rest of the controls of the form), a little trick is used here. We declare a Text view with an empty String, but with the same minimum width as the other labels above. A cleaner option would be to use a Spacer with a GeometryReader but this is beyond the scope of this tutorial.
  3. The Toggle is declared, followed by a spacer to push it to the left.

Now, we can fill the equipment update list. Please remove the comment and replace it with the following.

NewEquipmentRow(text: "New Mac", isOn: $infos.equipment.mac)
NewEquipmentRow(text: "New headset", isOn: $infos.equipment.headset)
NewEquipmentRow(text: "New keyboard", isOn: $infos.equipment.keyboard)
NewEquipmentRow(text: "New mouse", isOn: $infos.equipment.mouse)

The final EquipmentRow should look like this.

struct EquipmentRow: View {

    @ObservedObject var infos: Infos

    var body: some View {
        VStack(spacing: 5) {
            HStack {
                Text("Equipment:")
                    .frame(minWidth: labelsWidth, alignment: .trailing)
                Toggle(isOn: $infos.equipmentUpdate) {
                    Text("I wish to update my equipment")
                }
                Spacer()
            }
            if infos.equipmentUpdate {
                NewEquipmentRow(text: "New Mac", isOn: $infos.equipment.mac)
                NewEquipmentRow(text: "New headset", isOn: $infos.equipment.headset)
                NewEquipmentRow(text: "New keyboard", isOn: $infos.equipment.keyboard)
                NewEquipmentRow(text: "New mouse", isOn: $infos.equipment.mouse)
            }
        }
    }
}

We can now add it to the MainView below the DepartmentRow.

struct MainView: View {

    @StateObject var infos = Infos()
    @State private var showAlert = false var body: some View {
        Form {
            VStack {
                NameRow(
					value: $infos.firstName,
                   label: "First name")
                NameRow(
                    value: $infos.lastName,
                    label: "Last name")
                DepartmentRow(department: $infos.department)
                EquipmentRow(infos: infos)
            }
        }
    }
}

If you run the app, you should be able to check the equipment boxes and then check each piece of equipment box.


Send button

The last touch for this UI is to add a "Send" button that will save the user’s inputs and close the window. We already used buttons in the previous tutorials, so I’ll just put the implementation with some comments. Please add this right below the EquipmentRow(infos: infos) line.

HStack {
    // 1
    Spacer()
    // 2
    Button("Send") {
        // 3
        // TODO: save infos
        // 4
        NSApp.terminate(nil)
    }
}
  1. To align the button on the right in the form, a HStack is used with a spacer.
  2. The button is declared with a title and function to execute when it is clicked. Right now it only terminates the app, but we are going to implement the inputs saving in the next section.
  3. We will replace this comment with the user’s inputs export when it is implemented.
  4. To close the app window and terminate the app, we invoke NSApp.terminate(_:)

When running the app, you should have a UI similar to the first screenshots shown (except for the one with the validation warnings). When you click the « Send » button, the window should close and the app terminate.

It’s time to take a break! In the next section, you are going to get to know the powerful Codable protocol to save the user’s inputs.



Save the user’s inputs with Codable

A bit of theory

As we want to save the user’s input to a JSON file, the best solution in Swift is to work with the Codable protocols. When we want to export an object to a specific format, we can use the Encodable protocol. By making the object’s type implementing this protocol, we describe how we want the object to be transformed to the other format. If it’s commonly used for serialization, like saving to a JSON or Plist file, the Codable protocols can be used in other cases. For instance, it’s used by the Swift on server framework Vapor to provide information to a web page. With Scout, it allows to transform any type to an ExplorerValue.
In this tutorial though, we will keep things simple and only use features offered by the Encodableprotocol.

The counterpart of Encodable is Decodable and is used to describe how a type should be transformed from another format. The Codable protocol is only a type alias to group both Encodable and Decodable protocols:

typealias Codable = Encodable & Decodable

To transform an Encodable type to another format, we have to use an Encoder. This object will hold all the logic to make the transformation, as long as our type conforms to Encodable. For instance, we can use a Coder to transform an Encodable type to a JSON format, but we can also use another Coder to transform it to a Plist format, Yaml… The huge benefit of the Codable protocols is that we only have to describe how our type should be transformed from and to another format. Then any Encoder (or Decoder) can be used to effectively make this transformation.

For this tutorial, we want to export the Infos value to a JSON file. So we should use an Encoder that transforms an Encodable value to a JSON format. Fortunately, the Swift standard library comes with such a tool: JSONEncoder. If we rather were to decode, we would use JSONDecoder.

Similarly for the Plist format, the PropertyListEncoder and PropertyListDecoder are available in the standard library.


Encoding in practice

To encode a custom type like Infos, Swift offers several levels of freedom/complexity. When it’s possible, the compiler will automatically encode (or decode) a custom type for us, without further effort. For instance with a struct that only has Encodable properties, the compiler is able to know how to encode. Demonstrating all the possibilities to encode and the subtleties of the Codable protocol is beyond the scope of this tutorial. This page from the Apple documentation provides more details.
It’s also worth to say that we do not really have a choice in the method to use here. Because we want to encode an ObservableObject with @Published properties, we can’t rely on the compiler to generate the logic for us. We have to do it the hard way 💪
Don’t worry though, it’s not that complicated. And choosing the hardest path is a good opportunity to better understand the logic that the compiler would generate for us when possible.

The Codable protocols works with another one to uniquely identify properties: CodableKey. Most often, this protocol is implemented by aString enum defined in the type itself. So please add a new extension of Infos (below the class declaration closing curl bracket) with the following content.

extension Infos: Encodable {

    enum CodingKeys: String, CodingKey {
        case firstName, lastName, department, equipment
    }

Do note that the equipmentUpdate property is not specified, since exporting the equipment property is enough.

By doing so, we let the compiler know what names to use to encode (or decode) the properties of our Infos type. It might be enough if we were not dealing with @Published properties.
Unfortunately, the compiler accepts that we only declare the properties names when those properties are Encodable (or Decodable). Here, since we use the @Published property wrapper, the compiler is unable to understand how to encode the Infos properties. Maybe one day Apple will add this feature for us (making a Pusblished structure Codable when its wrapped value is Codable), but until then, we have to implement the logic ourselves. Thus please add the following function to the extension.

// 1
func encode(to encoder: Encoder) throws {
    // 2
    var container = encoder.container(keyedBy: CodingKeys.self)
    // 3
    try container.encode(firstName, forKey: .firstName)
    try container.encode(lastName, forKey: .lastName)
    try container.encode(department, forKey: .department)
    try container.encode(equipment, forKey: .equipment)
}
  1. The Encodable protocol requires the function encode(to:) function. As mentioned, the compiler will implement it for us when possible. Here, we declare that we implement it. The function can throw an error because encoding a property can fail, as denoted by all thetry statement below.
  2. To encode a specific group of properties, for instance all the properties of an object, the Codable protocols offer containers objects. Here we ask the encoder a container that is "keyed" by the CodingKeys enum we declared. Such containers most often correspond to dictionaries and rely on the CodingKey to act as their keys type. Similarly, it’s possible to ask for an unkeyedContainer that will rather correspond to an array. As this operation might fail with an error, we have to use a try keyword to make sure we are aware of that. If an error was thrown, the code below would not be executed and the function will exit with the error (allowing the calling code to know that the encoding failed and why).
  3. For each property of the Infos object, we make the container encode it using the property CodingKeys value. This way, the container will register the current value of the property with the name we provided in the enum.

So now, with all this work, the compiler should be happy, right?
No! It’s still complaining 😠

The thing is that the compiler already knows how to encode simple types like String or Int. But for the other custom types involved in the Infos class like Department or Equipment, there is no way for the compiler to encode them yet. Thus, it’s required that we make those two types conforming to Encodable. Fortunately enough, we can rely on the compiler doing the work for us this time as mentioned above. The compiler is smart enough to know how to encode them with the implementation we chose. It just requires us to clearly states those types as Encodable. So please add the Encodable protocol in the types declaration.

enum Department: String, CaseIterable, Identifiable, Encodable {
// ...
struct Equipment: Encodable {

Now - at last - the compiler should be happy. And we are ready to export the users’s input to a JSON file.


Export the inputs

The logic to export the inputs will be implemented inside a function named export(to:). Please add it inside the Infos class.

// 1
func export(to path: String) {
    // 2
    do {
        // 3
        let encoder = JSONEncoder()
        // 4
        encoder.outputFormatting = .prettyPrinted
        // 5
        let data = try encoder.encode(self)
        // 6
        FileManager.default.createFile(atPath: path, contents: data)
    } catch {
        // 7
        print("Something went wrong when exporting the inputs. \(error.localizedDescription)")
    }
}
  1. The function takes the file path where to export as a parameter.
  2. As explained, the encoding process might fail with an error. By wrapping the code inside a do {} statement here, we stop the error propagation and print it in the catch statement.
  3. The code to transform the value is quite simple. First we create a new JSONEncoder which will transform the infos into a JSON data value.
  4. Then, the created encoder is customized. Here, we simply specify that the JSON output should be prettyPrinted. This will make the JSON file more readable, with a nice identation.
  5. The JSON data is created from the infos object. This is where the function encode(to:) that we implemented will be called by the encoder. This call might throw, so the try statement is required.
  6. The JSON data is written at the path provided as a parameter using the FileManager.createFile(atPath:contents:) function. In case of the file already exist, it will be overwritten. This function can also fail, for instance if we do not have the permission to write to a file. Thus, a try statement is also required.
  7. If the encoding function or the file creation functions fail, the error is caught here and printed. Logging the error this way can be enough for debugging, but it’s not sufficient to inform the user about the problem. In one of the challenges, you will be asked to show an alert to the user, with a relevant message.

To use this function, we only need to call it in the "Send" button function. Replace the "TODO: save inputs" by the function call.

HStack {
    Spacer()
    Button("Send") {
		infos.export(to: "/Users/Shared/inputs.json")
    }
}

Note that you are free to choose another file path of your preference. Make sure though that you are allowed to write there.

Now, when the “Send” button is clicked, the current inputs should appear in a JSON file at the path you specified.

{
  "firstName" : "Arthur",
  "lastName" : "Fleck",
  "department" : "Sales",
  "equipment" : {
    "headset" : false,
    "keyboard" : false,
    "mac" : false,
    "mouse" : false
  }
}

Hurrah! 🙌


The last part is optional, since this tutorial is quite long already. It will offer a way to perform data validation, and how to show a relevant message to the user. If you are not interested, you can jump to the recap and try the challenge. Otherwise, consider taking a break or coming back to this part in a few days. We already have a nice UI with input exportings, so we can call it a day!



Validating the inputs

When asking the user to fill a form, it’s very common to ensure their inputs is correct. For instance, that a phone number is composed of the proper digits, or that an email address is valid.
In this tutorial, we are going to ensure that the first and last names are not empty. It’s limited but it’s enough for us to see how the UI can be updated to inform the user.

To setup this validation, the work can be split in two distinct tasks:
- First, we have to decide when to show a warning, It’s important to not overwhelm the user with warnings every time they enter an incorrect piece of information. Here, we will rather display a warning only when the user clicks the “Send” button. Moreover, only when the user hover the warning icon will we display the text with an explanation.
- Second, how should we display the warning? They are several possibilities, like an alert, a subtext below the field, and more. Most often, I think it’s better to make the warning appear near the field it’s associated with. Thus, it’s easier to understand where the error can be found. For this tutorial, we will add a small red cross inside the text field that has an incorrect value. When the user will hover the cross, a popover with an explicative text will appear.

Let's start with the logic. Once it will be ready, it will be easier to implement the view. Keep on!


Implement the logic

When the user clicks the “Send” button, it could be useful if the Infos object had a validate function returning a boolean to indicate whether the current infos are valid or not. The button function could then decide to prevent further actions like the inputs export.
So please this function at the end of the Infos class.

func validate() -> Bool {
    // will be completed soon
}

Also, add the function call inside the “Send” Button action function, at the very beginning.

Button("Send") {
    guard infos.validate() else { return }
   
    // export data
    infos.export(to: "/Users/Shared/inputs.json")
}

This way, if the infos object is not valid, the code to export the inputs will not be called. The compiler should be complaining but don’t worry, we are about to make it happy very soon.

So, what can go wrong in the form? As mentioned above, we will ensure that the first and last name fields are not empty. This will be modeled in the Infos class with two computed properties: isFirstNameValid and isLastNameValid. So please add them to the Infos class.

var isFirstNameValid: Bool { !firstName.isEmpty }
var isLastNameValid: Bool { !lastName.isEmpty }

In the final implementation of this project, the logic of theInfos class is separated into different extensions to make the code easier to navigate through. Feel free to organise your code the way that you find the more readable.

As we are going to need to check whether the names field are valid are valid in several places, let’s add a general isValid property to group them.

var isValid: Bool { isFirstNameValid && isLastNameValid }

Now, we can use this value in the validate() function. So please replace comment in the function with this.

func validate() -> Bool {
	return isValid
}

Now, when the user clicks the send button, nothing should happen. That’s great, but it would be better to display the warning to the user. So let’s implement the logic for that. That’s the last part before the UI, I promise!

First, we will need a variable to know whether the warnings should be shown. As mentioned above, the warnings should not appear if the user has not clicked the "Send" button at least once. Otherwise, this might feel overwhelming. So, please add a shouldDisplayWarnings property to Infos in its declaration (extensions cannot contain stored properties in case you tried).

@Published private var shouldDisplayWarnings = false

The property is private because we will expose other ones to the view. That said, the property is a @Published one because we want the view to update when it is modified.

Now, we can add the final two properties to know if the warnings for each name text fields should be visible. You can add those in the same block that the validation properties.

var shouldDisplayWarningForFirstName: Bool {
    shouldDisplayWarnings && !isFirstNameValid
}

var shouldDisplayWarningForLastName: Bool {
    shouldDisplayWarnings && !isLastNameValid
}

Rather than declaring a computed variable for each property, we could have use key paths here, but it’s out of the scope of this tutorial.

Lastly, we should set shouldDisplayWarnings when the user tries to save the inputs. Thus, the validate function is a perfect spot.

func validate() -> Bool {
    shouldDisplayWarnings = !isValid
    return isValid
}

Whenever this function is called, we change the value of shouldDisplayWarnings which will either allow the warnings to be shown or hidden.

Everything’s ready for the views, so let’s set them up!


Implement the views

To make the red cross appear in the TextField, we will use an overlay modifier. This function takes a View as a parameter and puts it in front of the view it applies to. That’s perfect for our case, and simple to use.
The hardest part here is to implement the view that goes in the overlay. We will use a SF symbol for the cross so this should be simple. However, there are two new features we are going to use: a popover and a gesture recognizer for hovering.
The popover takes a View as its content, so we’ll pass it a simple Text. The hover recognizer takes a function that will be executed when the hovering starts or stops. Here it is with comments.

struct WarningPopover: View {
    // 1
    var text: String
    // 2
    var shouldDisplay: Bool
    // 3
    @State private var showPopover = false

    var body: some View {
        HStack { // 4
            Spacer()
            // 5
            if shouldDisplay {
                // 6
                Image(systemName: "xmark.octagon")
                    .foregroundColor(.red)
                    .padding(2)
                    // 7
                    .popover(isPresented: $showPopover) {
                        Text(text)
                            .padding()
                    }
                    // 8
                    .onHover { (hoverring) in
                        showPopover = hoverring
                    }
            }
        }
    }
}
  1. The text property will be used for the warning text in the popover
  2. The shouldDisplay specifies when the cross should be visible.
  3. To make the popover visible when hovering, a private State property is used.
  4. To push the cross at the trailing edge, a HStack will hold it as well as a Spacer
  5. Similarly with the equipment list, an if statement is used to make the view visible depending on the shouldDisplay property.
  6. The red cross is drawn here using the SF Symbol named “xmark.octagon”. The color is set to red and some padding is added
  7. The popover is a modifier that applies on the view. It takes a binding boolean value to know when to be presented, and a View to present
  8. onHover allows to specify the function to execute when the hovering changes. It offers a parameter in the function to know whether the view is being hovered or not.

Now that the WarningPopover view is declared, we can add it to the NameRow in an overlay. Add this right below the TextField:

TextField(label, text: $value)
    .overlay(
        WarningPopover(
            text: "Invalid value. Please provide a non empty value",
            shouldDisplay: shouldDisplayWarning)
        )
    )

The compiler should complain, so let’s make it happy. Add the property shouldDisplayWarning to the NameRow view.

struct NameRow: View {

    @Binding var value: String
    let label: String
    var shouldDisplayWarning: Bool
    // ...

Finally, we now have to provide the value for the new property at initialisation in the MainView.
Please remove the lines

NameRow(
    value: $infos.firstName,
    label: "First name")
NameRow(
    value: $infos.lastName,
    label: "Last name")

For the new ones

NameRow(
    value: $infos.firstName,
    label: "First name",
    shouldDisplayWarning: infos.shouldDisplayWarningForFirstName)
NameRow(
    value: $infos.lastName,
    label: "Last name",
    shouldDisplayWarning: infos.shouldDisplayWarningForLastName)

Careful not to mix first and last properties!

When running the app, you should see a cross appear in a text field that has an empty value, and a popover with the warning text when hovering the cross.


We did it!



Recap

A lot of stuff in this tutorial! Building a form interface in SwiftUI, and then using Codable to export the user inputs. Finally, the optional validation part to ensure the inputs are correct.

Possible use cases

Those features could be really useful to offer your end-users a nice way to fill information, and then retrieve them. A survey for your company? You could push your script on your users devices and execute it to ask them to fill the survey.

Explore the controls

We only covered some controls that can be used in a form. Feel free to experiment other ones, or to customise them. For instance, it’s possible to require a specific input in the TextField, like numeric characters only. Don’t forget that with SwiftUI powerful state management, it’s easy to make some parts of the form appear conditionally, like we did for the equipment list.

Codable

If you feel lost with Codable, don’t worry! We did it the hard way since we had no choice. Most often, you can find yourself only adding an Encodable declaration to your type, and rely on the compiler to do the work for you. Doing it manually at least provided an opportunity to understand how it’s done. And if it’s seem complicated, don’t feel lost. It will come with time. If you would like to learn how to rather decode a type, I would advice you to take a look at this tutorial.



Challenges

You can find the solution for the challenges in the Challenges folder.

Export to a Plist format

Difficulty: +
If you prefer Plist files, you can try to use the PropertyListEncoder to encode the user’s inputs to a Plist file rather than a JSON one.

Alert for an exporting error

Difficulty: ++
Currently, if the export of the inputs fails, the error is logged but nothing else, which is a poor user experience.
Add an alert modifier to the MainView to show a proper message to the user, like contacting the support.

Read the user’s input

Difficulty: +++
In this tutorial, we only covered the encoding part. If you are feeling adventurous, you can try to allow to read user’s input from an exported file. You can write a function that will read the content of a file at a specific path (we’ll cover in another tutorial how to read input’s from the command-line). Then the content of the file should be deserialized to an Infos object. You might find those tutorials interesting:
- Apple Doc
- Ray Wenderlich
- SwiftLee

Encoding and decoding are great tools to add to your belt. Especially when it comes to share data. Being at ease with those concepts can really be a valuable skill, while it can be hard to properly master them. Don’t worry if it takes time, and don’t hesitate to ask if you have a question about this topic.


Thanks for reading!

📡

So what do you think of tutorial? If you enjoyed reading this content, feel free to share it on Twitter. If you want to reach out to me or to know when new posts are available, you can find me on Twitter or send me an email.