2️⃣GET Requests

Fall 2023 | Vin Bui

In this section, we will be using Alamofire and callbacks to perform network requests, specifically GET requests.

Creating a NetworkManager Class

It is common in iOS development to create a class that represents our backend calls. Let’s create this class and name it NetworkManager. Make sure to import Alamofire.

import Alamofire

class NetworkManager {
    /// Shared singleton instance
    static let shared = NetworkManager()
    
    // Prevent other instances from being created
    private init() { }
}

We create this static variable called shared that holds a singleton and make the initializer private. This guarantees that only one instance of this class is created. To access this variable, we can simply use NetworkManager.shared

Creating a GET Request

Inside of this NetworkManager class, we write functions to be called by our application. These functions will use Alamofire to send HTTP requests.

Assume there is a struct called Member that represents an AppDev member.

struct Member: Codable {
    let name: String
    let subteam: String
    let position: String
}

We can write the following code to fetch the AppDev roster.

// 1. Create the function
func fetchRoster(completion: @escaping ([Member]) -> Void) {
    // 2. Specify the endpoint
    let endpoint = "<Enter URL String Here>"
    
    // 3. Create a decoder
    let decoder = JSONDecoder()
    // decoder.dateDecodingStrategy = .iso8601 // Only if needed
    // decoder.keyDecodingStrategy = .convertFromSnakeCase // Only if needed
    
    // 4. Create the request
    AF.request(endpoint, method: .get)
        .validate()
        .responseDecodable(of: [Member].self, decoder: decoder) { response in
	    // 5. Handle the response
            switch response.result {
            case .success(let members):
                print("Successfully fetched \(members.count) members")
                completion(members)
            case .failure(let error):
                print("Error in NetworkManager.fetchRoster: \(error)")
            }
        }
}

Let’s break down this code.

  1. Create a function called fetchRoster that takes in a callback (completion handler) as an argument. This callback takes in an array of Member objects.

  2. Specify the endpoint which is the URL that we will call to fetch the data. We can test this with Postman.

  3. Create a decoder to decode the data. If our object contains a Date property, then we need to specify the date decoding strategy. If the JSON contains fields with snake_case, then we also need to specify the key decoding strategy.

  4. Create the request using Alamofire.

    • Pass in the endpoint and specify the method (.get for GET, .post for POST, etc).

    • Validate the request to ensure that the status code is 2xx and if the content type matches.

    • Decode the response to [Member] using the decoder we created in Step 3.

  5. Perform a switch statement on the response’s result.

    • If successful, pass to the callback the decoded response. A print statement is optional but recommended.

    • If failed, print an error statement about the error.

Note that we do not have to create the decoder inside of the function call. We can create a JSONDecoder object and store it as a property in our NetworkManager class to be used for all functions. The same can be applied to the endpoint variable containing a String value of our endpoint.

Calling the Network Request

There is a problem with this code that will be discussed in the next section below.

NetworkManager.shared.fetchRoster { fetchedMembers in
    // Do something with the data such as...
    self.members = fetchedMembers
    
    DispatchQueue.main.async {
        // Perform UI updates such as...
        self.collectionView.reloadData()
    }
}

We call the function that we just created inside of the NetworkManager class. Remember to use NetworkManager.shared. To access the value passed into the callback, we simply use the in keyword. For example, earlier we passed in an array of Member objects to the callback. In this case, fetchedMembers is holding the value that was passed in. We can call the variable whatever we want but it is very helpful to make it representative of the data.

Then, we do whatever we want with the data that we just fetched. If we are referencing a property outside of this function call, we will need to use self.

Next, if we need to perform any UI updates such as updating a collection view, we put that in a DispatchQueue.main.async block. This runs any code inside of the block on the main queue asynchronously. This is an advanced topic but the reason for this is so that the UI does not freeze while we are sending a network request.

Using weak self

Although the above code works, there is one major issue: we are retaining a strong reference to self inside of a closure which can cause retain cycles (causing memory leaks). To go around this, we will retain a weak reference to self inside of the closure and then unwrap a strong reference. We do this by adding [weak self] in our closure and using a guard let to unwrap a strong reference.

NetworkManager.shared.fetchRoster { [weak self] fetchedMembers in
    // Unwrap a strong reference
    guard let self = self else { return }

    // Do something with the data such as...
    self.members = fetchedMembers
    
    DispatchQueue.main.async {
	// Perform UI updates such as...
        self.collectionView.reloadData()
    }
}

You may have noticed that we also use weak when creating our delegate properties. This is an advanced topic that we will discuss later, but for now, make sure to use weak self every time we need to use self in our networking calls.

Last updated