SwiftUI and REST (Episode II)

In the first episode SwiftUI and REST (Episode I), we explored how to use a REST API with GET requests. In this episode, we’ll learn how to perform POST requests and use a bearer token to access REST APIs that require authentication. At the end of the post, you’ll find a link to the GitHub project, which includes the code for the mobile application as well as a simple server for testing purposes.

Post

First, let’s define the structure we’ll use, starting with the information to be sent to the server for login (note that the server already has a user x@y.com with the password 123456).

struct UserInfo: Codable {
    var email: String
    var password: String
}

This structure is Codable, meaning it can both decode and encode. In this case, we’ll use it to encode the information to be sent with the POST request.

let encoder = JSONEncoder()
let credentials = UserInfo(email: username, password: password)
let data = try encoder.encode(credentials)

The POST request should return a token that must be used to call the other APIs exposed by the server. To retrieve it, define the following structure:

struct LoginResponse: Codable {
    var user: UserAuth
}

struct UserAuth: Codable {
    var id: String?
    var email: String?
    var token:  String?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeIfPresent(String.self, forKey: .id)
        email = try container.decodeIfPresent(String.self, forKey: .email)
        token = try container.decodeIfPresent(String.self, forKey: .token)
    }
}

Now, let’s merge everything into the LoginView:

struct LoginView: View {
    @State var password = ""
    @State var username = ""
    @Binding var token: String
    var body: some View {
        VStack {
            TextField("Username", text: $username)
            SecureField("Password", text: $password)
            Button("Login") {
                Task {
                    await login()
                }
            }
        }.padding()
    }
    func login() async {
        
        guard let url = URL(string: "YOUR_IP:8080/api/v1/user/login") else {
            print("Invalid URL")
            return
        }
        do {
            var request = URLRequest(url: url)
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpMethod = "POST"
            let encoder = JSONEncoder()
            let credentials = UserInfo(email: username, password: password)
            let data = try encoder.encode(credentials)
            request.httpBody = data
            let (dataResponse, response) = try await URLSession.shared.data(for: request)
            
            let httpResponse = response as? HTTPURLResponse
            print(httpResponse!.statusCode)
            
            if httpResponse!.statusCode != 200 {
                print("Login error")
                return
            }
            let decoder = JSONDecoder()
            do {
                print(response)
                let decodedResponse = try decoder.decode(LoginResponse.self, from: dataResponse)
                let resultString = String(data: dataResponse, encoding: .utf8)
                print("Data in String: \(String(describing: resultString))")
                print(decodedResponse.user.token ?? "")
                self.token = decodedResponse.user.token ?? ""
            } catch let DecodingError.dataCorrupted(context) {
                print(context)
            } catch let DecodingError.keyNotFound(key, context) {
                print("Key '\(key)' not found:", context.debugDescription)
                print("codingPath:", context.codingPath)
            } catch let DecodingError.valueNotFound(value, context) {
                print("Value '\(value)' not found:", context.debugDescription)
                print("codingPath:", context.codingPath)
            } catch let DecodingError.typeMismatch(type, context)  {
                print("Type '\(type)' mismatch:", context.debugDescription)
                print("codingPath:", context.codingPath)
            } catch {
                print("error: ", error)
            }
        } catch {
            print("Invalid data")
        }
    }
}

This view defines three variables: username, password, and token. Please note that token is defined as @Binding because we want this token to be used also in other views that call additional APIs.

Here is the piece of code

let resultString = String(data: dataResponse, encoding: .utf8)
print("Data in String: \(String(describing: resultString))")

It can be helpful to check what information is received from the server.

There’s another trick that’s used for the preview, which is necessary when a view uses a @Binding:

#Preview {
    @State var t = ""
    return LoginView(password: "", username: "", token: $t)
}

Now, let’s take a look at the main view:

struct ContentView: View {
    @State var token: String = ""
    var body: some View {
        NavigationStack {
            if token.count == 0 {
                LoginView(token: $token)
            } else {
                BooksView(token: $token)
            }
        }
    }
}

The token, which will be bound by the LoginView, is defined here. Therefore, this view is displayed when the token is empty. Otherwise, the BooksView is shown, which receives the token as a parameter.

Now, let’s take a look at this last view, but first, let’s examine the structures used:

struct ResponseBook: Codable { // the response for the books operation
    var books: Array<Book>?
    var error: String?
    var total: Int
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        books = try container.decodeIfPresent(Array<Book>.self, forKey: .books)
        total = try container.decode(Int.self, forKey: .total)
        error = try container.decodeIfPresent(String.self, forKey: .error)
    }
}

struct Book: Codable {
    var id: String
    var title: String
    var subtitle: String?
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        title = try container.decode(String.self, forKey: .title)
        subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
    }
}

If you have any doubts about the init function, please refer to the previous post.

Call API with authentication

What we aim to do is receive the list of books for our user, identified by the token, and return a JSON object that contains the list of books, the number of books, and an error message if one is present. The books should be displayed in a list.

Here’s the first piece of code:

struct BooksView: View {
    @State var books = [Book]()
    @Binding var token: String
    let host = "YOUR_IP:8080"
    var body: some View {
        List {
            ForEach(books, id: \.id) { item in
                Text(item.title)
            }
        }.navigationTitle("Books")
            .task {
                await loadBooks()
            }
    }

For each book show the title, the books are loaded with this function:

    
        guard let url = URL(string: host + "/api/v1/books") else {
            print("Invalid URL")
            return
        }
        do {
            var request = URLRequest(url: url)
            request.setValue( "Bearer \(token)", forHTTPHeaderField: "Authorization")
            request.httpMethod = "GET"

            let (dataResponse, _) = try await URLSession.shared.data(for: request)
            let decoder = JSONDecoder()
            do {
                // process data
                let decodedResponse = try decoder.decode(ResponseBook.self, from: dataResponse)
                let resultString = String(data: dataResponse, encoding: .utf8)
                print("Data in String: \(String(describing: resultString))")
                if let books = decodedResponse.books {
                    self.books = books
                }
            } catch let DecodingError.dataCorrupted(context) {
                print(context)
            } catch let DecodingError.keyNotFound(key, context) {
                print("Key '\(key)' not found:", context.debugDescription)
                print("codingPath:", context.codingPath)
            } catch let DecodingError.valueNotFound(value, context) {
                print("Value '\(value)' not found:", context.debugDescription)
                print("codingPath:", context.codingPath)
            } catch let DecodingError.typeMismatch(type, context)  {
                print("Type '\(type)' mismatch:", context.debugDescription)
                print("codingPath:", context.codingPath)
            } catch {
                print("error: ", error)
            }
        } catch {
            print("Invalid data")
        }
    }

The new thing is:

var request = URLRequest(url: url)

request.setValue( "Bearer \(token)", forHTTPHeaderField: "Authorization")

request.httpMethod = "GET"

With this code, set the bearer token and specify that we are making a GET request. The rest of the code is very similar to what we used in the previous example.

In the code, replace YOUR_IP with the IP address of the computer that runs the server. To run the server, you need to install Docker Desktop. After that, go to the my-library-be directory and execute docker-compose up.

https://github.com/niqt/rest-swiftui

share this post with friends

Picture of Nicola De filippo

Nicola De filippo

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve

Leave a comment

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

Who I am

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve.

Follow Me Here

Get The Latest Updates

Periodically receive my super contents on coding and programming

join the family;)

Recent Posts