Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by StellarSentinel844

How can I create a generic API loader in Swift for multiple endpoints?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I have a simple iOS SwiftUI app that calls 7 different API endpoints. I wrote a function to perform an API call and decode the JSON response, but I need to reuse it for different endpoints. Passing the endpoint URL string is straightforward, but I'm not sure how to pass different Codable data models for each endpoint.

I considered using an enum, but couldn't find a clear example for my case. Below is my current API loader function and the data model for HockeyTimeZones as an example.

SWIFT
func apiLoader(urlString: String) { // Create API URL //let url = URL(string: "https://api-hockey.p.rapidapi.com/timezone") let url = URL(string: urlString) guard let requestUrl = url else { fatalError() } // Create URL Request var request = URLRequest(url: requestUrl) // Specify Headers to use let headers = [ "x-rapidapi-key": "REDACTED", "x-rapidapi-host": "REDACTED.p.rapidapi.com" ] // Specify HTTP Method to use request.httpMethod = "GET" request.allHTTPHeaderFields = headers // Send HTTP Request let task = URLSession.shared.dataTask(with: request) { (data, response, error) in // Check if Error took place if let error = error { print("Error took place \(error)") return } // Convert HTTP Response Data to a simple String if let data = data.self { do { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .formatted(dateFormatter) let hockeyTimeZones = try decoder.decode(HockeyTimeZones.self, from: data) hockeyTimeZones.response!.forEach { timezone in print(timezone) } //return hockeyTimeZones } catch { print("Failed to load: \(error)") //return Void() } } } task.resume() //return HockeyTimeZones() }
SWIFT
@Observable class HockeyTimeZones: ObservableObject, Codable { var get : String? = nil var parameters : [String]? = [] var errors : [String]? = [] var results : Int? = nil var response : [String]? = [] enum CodingKeys: String, CodingKey { case _get = "get" case _parameters = "parameters" case _errors = "errors" case _results = "results" case _response = "response" } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) get = try values.decodeIfPresent(String.self , forKey: ._get ) parameters = try values.decodeIfPresent([String].self , forKey: ._parameters ) errors = try values.decodeIfPresent([String].self , forKey: ._errors ) results = try values.decodeIfPresent(Int.self , forKey: ._results ) response = try values.decodeIfPresent([String].self , forKey: ._response ) } init() { } }

I would like to refactor this function to use generics so I can pass the data model type as a parameter, allowing it to decode the response into the correct model for each endpoint. Any suggestions or examples on how to achieve this would be greatly appreciated.

0

Comments 0

Answers 2

Views 75

2 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

You can solve this by making your API loader function generic. This way, you pass the type that conforms to Codable (or Decodable) as a parameter to your function, and then use that type when decoding. Here's an example of how you might refactor your function:


SWIFT
func apiLoader<T: Codable>(urlString: String, type: T.Type, completion: @escaping (Result<T, Error>) -> Void) { // Create API URL guard let url = URL(string: urlString) else { fatalError("Invalid URL string") } // Create URL Request var request = URLRequest(url: url) // Specify Headers to use let headers = [ "x-rapidapi-key": "REDACTED", "x-rapidapi-host": "REDACTED.p.rapidapi.com" ] request.httpMethod = "GET" request.allHTTPHeaderFields = headers // Send HTTP Request URLSession.shared.dataTask(with: request) { data, response, error in // Check if Error took place if let error = error { completion(.failure(error)) return } // Ensure we got some data guard let data = data else { completion(.failure(URLError(.badServerResponse))) return } do { // Prepare decoder with custom settings. let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .formatted(dateFormatter) // Decode the data to the provided type let decodedObject = try decoder.decode(type, from: data) completion(.success(decodedObject)) } catch { completion(.failure(error)) } }.resume() }

How to Use This Function

Assuming you have your data model declared as before:

SWIFT
@Observable class HockeyTimeZones: ObservableObject, Codable { var get: String? = nil var parameters: [String]? = [] var errors: [String]? = [] var results: Int? = nil var response: [String]? = [] enum CodingKeys: String, CodingKey { case _get = "get" case _parameters = "parameters" case _errors = "errors" case _results = "results" case _response = "response" } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) get = try values.decodeIfPresent(String.self, forKey: ._get) parameters = try values.decodeIfPresent([String].self, forKey: ._parameters) errors = try values.decodeIfPresent([String].self, forKey: ._errors) results = try values.decodeIfPresent(Int.self, forKey: ._results) response = try values.decodeIfPresent([String].self, forKey: ._response) } init() { } }

You can call your API loader for HockeyTimeZones like this:

SWIFT
apiLoader(urlString: "https://api-hockey.p.rapidapi.com/timezone", type: HockeyTimeZones.self) { result in switch result { case .success(let hockeyTimeZones): hockeyTimeZones.response?.forEach { timezone in print(timezone) } case .failure(let error): print("Failed to load data: \(error)") } }

Explanation

  • Generic Parameter (T):
    The function is generic over T where T conforms to Codable. This allows you to pass any Codable type (like HockeyTimeZones or any of your other 6 data models) to the function.

  • Type Parameter:
    The parameter type: T.Type is used to specify the specific model type to decode. For example, HockeyTimeZones.self is passed when you want to decode the response into a HockeyTimeZones object.

  • Completion Handler:
    We use a result-based completion handler so that you can handle success (with the decoded object) or failure (with an error).

This approach keeps your API loader flexible and reusable for all endpoints without having to duplicate code for each model type.

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by UranianDiscoverer383 1 month ago

0

When you have multiple API calls that return different types of responses that
you want to decode, use Swift generics

For example:

SWIFT
func getData<T: Decodable>(from urlString: String) async -> T? { guard let url = URL(string: urlString) else { print(URLError(.badURL)) return nil // <-- todo, deal with errors } do { let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { print(URLError(.badServerResponse)) return nil // <-- todo, deal with errors } return try JSONDecoder().decode(T.self, from: data) // <--- here } catch { print(error) return nil // <-- todo, deal with errors } }

Call it like this:

SWIFT
.task { let theResponse: ApiResponse? = await getData(from: "https://...") }

Note the important type ApiResponse? in let theResponse: ApiResponse? = ....,
this tells the type getData(...) should use for decoding and to return.

Specific example code:

SWIFT
struct ContentView: View { @State private var timeZones: TzResponse? var body: some View { List(timeZones?.response ?? [], id: \.self) { tz in Text(tz) } .task { timeZones = await getData(from: "https://api-hockey.p.rapidapi.com/timezone") } } func getData<T: Decodable>(from urlString: String) async -> T? { let apikey = "YOUR-API-KEY-HERE" guard let url = URL(string: urlString) else { print(URLError(.badURL)) return nil // <-- todo, deal with errors } var request = URLRequest(url: url) request.addValue("application/json", forHTTPHeaderField: "Accept") request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("\(apikey)", forHTTPHeaderField: "X-RapidAPI-Key") request.addValue("api-hockey.p.rapidapi.com", forHTTPHeaderField: "X-RapidAPI-Host") do { let (data, response) = try await URLSession.shared.data(for: request) //print(String(data: data, encoding: .utf8) as AnyObject) let decodedData = try JSONDecoder().decode(T.self, from: data) print("\n---< decodedData: \(decodedData)") return decodedData } catch { print(error) return nil // <-- todo, deal with errors } } } struct TzResponse: Codable { let errors: [String] let parameters: [String] let response: [String] let get: String let results: Int }

No comments yet.

Discussion

No comments yet.