Example Project

Online version of the directory app in SwiftUI

Click the button above to download the code for our next example project. This project is an online version of the Directory app. This version of the app communicates with a web service that stores all of the directory entries for all of the users who use the app.

A gateway class

Apps that store their data locally typically use a model class that manages storing and retrieving the app's data. The equivalent of a model class for apps that work with a back end is a gateway class. Here is an outline of the gateway class I constructed for this app:

class Gateway : ObservableObject {
    @Published var people : [Person] = []
    var key : String = ""

    func newUser(name n: String,password p: String) async -> Bool {
        // Code to create a new user goes here
    }

    func login(name n: String,password p: String) async -> Bool {
        // Code to log in an exising user goes here
    }

    func refresh() async {
        // Code to fetch our directory entries goes here
    }

    func addPerson(_ p : Person) async {
        // Code to add a new Person to our directory goes here
    }

    func removePerson(at n : Int) async {
        // Code to remove a directory entry goes here
    }
}

The class has two important properties: the user's key and a list of Person objects. The key will be set on a successful login or after creating a new user. The refresh() method will send a request to the server to fetch the user's list of directory entries. The result of that query will get stored in the people array, which we publish to the interface of the app.

Note that the refresh(), addPerson(), and removePerson() methods are all marked async. This means that we have to be careful to only call those methods inside of tasks.

Handling POST and DELETE

In the SwiftUI weather app I showed some examples of how to use async and await to manage GET requests. For this app we will also need to handle POST operations that post new Person objects to the user's directory and DELETE operations to remove Person objects.

Here is the code for our gateway's addPerson() method:

func addPerson(_ p : Person) async {
    guard !key.isEmpty else { return }
    let urlStr = "https://cmsc106.net/directory/people/\(key)"
    guard let url = URL(string: urlStr) else { return }
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    do {
        let jsonData = try JSONEncoder().encode(p)
        let (_, response) = try await URLSession.shared.upload(for: request, from: jsonData)

        guard (response as! HTTPURLResponse).statusCode == 200 else { return  }
        await refresh()
    } catch {
    }
}

The main difference between GET and POST requests is that GET requests use the URLSession.shared.data() method, while POST requests use the URLSession.shared.upload() method instead. The data() method takes a single URL object as its parameter, while the upload() methods takes both a URLRequest object and the data to post as its parameters. The URLRequest object contains the request URL along with additional information that designates this as a POST request whose body contains JSON code.

The removePerson() sends a DELETE request to the server:

func removePerson(at n : Int) async {
    guard !key.isEmpty else { return }
    let toRemove = people[n]
    let urlStr = "https://cmsc106.net/directory/people/\(key)/\(toRemove.id)"
    guard let url = URL(string: urlStr) else { return }
    var request = URLRequest(url: url)
    request.httpMethod = "DELETE"
    do {
        let (_, response) = try await URLSession.shared.data(for: request)
        guard (response as! HTTPURLResponse).statusCode == 200 else { return }
        await refresh()
    } catch {
    }
}

Like a GET request, a DELETE request also uses URLSession.shared.data(). The difference between the two is that for a GET request we will pass the data() method a URL object, while for a DELETE request we pass a URLRequest object that contains the URL and identifies this as a DELETE request.

Interacting with the gateway

Since this app interacts with a back end that requires the user to log in before using the directory, the first view in the app is a log in view. Here is the code for that first view:

struct LoginView: View {
    @StateObject var gateway : Gateway
    @State var goToMain = false
    @State var user : String = ""
    @State var password : String = ""

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: DirectoryView(gateway:gateway), isActive: $goToMain) { EmptyView() }
                HStack {
                    Text("User:").frame(width:100,height:20,alignment: .trailing)
                    TextField("",text:$user).textFieldStyle(.roundedBorder).disableAutocorrection(true)
                }
                HStack {
                    Text("Password:").frame(width:100,height:20,alignment: .trailing)
                    SecureField("",text:$password).textFieldStyle(.roundedBorder)
                }
                HStack {
                    Button("New User"){
                        Task {
                        goToMain = await gateway.newUser(name: user, password: password)
                        }
                    }.buttonStyle(.borderedProminent).padding()
                    Spacer()
                    Button("Log In"){
                        Task {
                            goToMain = await gateway.login(name: user, password: password)
                        }
                    }.buttonStyle(.borderedProminent).padding()
                }
            }
        }
    }
}

This login view uses a NavigationLink to navigate to the main view in the app. The NavigationLink in this case is an invisible link that uses the goToMain state variable to trigger the navigation.

The action closures for both the Log In and New User buttons contain Tasks that handle the login process. Since both the gateways newUser() and login() methods are marked async we need to be careful to run them inside tasks. Both of these methods will return a boolean value to indicate whether or not they were successful. In this case, we assign that boolean result to the goToMain property: in case of a successful login this will trigger the navigation to the app's main view. (The login() method will also call the gateway's refresh() method to fetch the user's directory items upon a successful login.