SwiftUI中复用API调用函数适配多端点及不同解码类的方法
复用SwiftUI API调用函数:动态适配不同解码类型
我开发了一个简易的iOS SwiftUI应用,需要调用7个API端点。已经写好用于API调用和数据响应解码的函数,要复用这个函数的话,需要替换端点URL字符串以及数据解码对应的类。传递URL很简单,但不知道怎么为每个端点向API函数传入不同的数据类。找了很多示例都不符合我的场景,考虑过用enum但不清楚具体实现方式。以下是当前的API调用函数和数据类代码:
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() }
@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() { } }
方案一:泛型函数实现动态解码
核心思路是把apiLoader改成泛型函数,让它支持任意遵循Codable协议的类型,同时通过回调(completion handler)返回异步请求的结果。
修改后的函数代码:
func apiLoader<T: Codable>(urlString: String, completion: @escaping (Result<T, Error>) -> Void) { guard let requestUrl = URL(string: urlString) else { completion(.failure(NSError(domain: "Invalid URL", code: -1, userInfo: nil))) return } var request = URLRequest(url: requestUrl) let headers = [ "x-rapidapi-key": "REDACTED", "x-rapidapi-host": "REDACTED.p.rapidapi.com" ] request.httpMethod = "GET" request.allHTTPHeaderFields = headers let task = URLSession.shared.dataTask(with: request) { data, _, error in if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(NSError(domain: "No data received", code: -2, userInfo: nil))) return } 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 decodedData = try decoder.decode(T.self, from: data) completion(.success(decodedData)) } catch { completion(.failure(error)) } } task.resume() }
调用方式示例
比如调用HockeyTimeZones类型的接口:
apiLoader(urlString: "https://api-hockey.p.rapidapi.com/timezone") { (result: Result<HockeyTimeZones, Error>) in switch result { case .success(let timezones): timezones.response?.forEach { print($0) } // 这里可以更新UI,比如把timezones赋值给@Observable变量 case .failure(let error): print("加载失败:\(error)") } }
如果是另一个接口,比如自定义的HockeyTeams类:
apiLoader(urlString: "https://api-hockey.p.rapidapi.com/teams") { (result: Result<HockeyTeams, Error>) in switch result { case .success(let teams): // 处理球队数据 case .failure(let error): print("加载失败:\(error)") } }
方案二:用Enum统一管理API端点
如果想更规范地管理7个API端点,可以定义一个枚举,把每个端点的URL和对应的解码类型绑定在一起,避免手动输入URL出错。
示例枚举代码:
enum APIEndpoint { case timezone case teams case players(Int) // 带参数的端点示例 // 返回对应端点的URL var urlString: String { switch self { case .timezone: return "https://api-hockey.p.rapidapi.com/timezone" case .teams: return "https://api-hockey.p.rapidapi.com/teams" case .players(let teamId): return "https://api-hockey.p.rapidapi.com/teams/\(teamId)/players" } } }
用Enum调用泛型函数
let endpoint = APIEndpoint.timezone apiLoader(urlString: endpoint.urlString) { (result: Result<HockeyTimeZones, Error>) in // 处理结果 }
额外优化:简化数据类代码
你的HockeyTimeZones类有可以优化的地方:
- iOS 17+中,
@Observable宏已经替代了ObservableObject,不需要同时继承 - 因为你用了
decoder.keyDecodingStrategy = .convertFromSnakeCase,JSON中的蛇形键会自动映射到驼峰命名的属性,所以不需要自定义CodingKeys,代码可以简化:
@Observable class HockeyTimeZones: Codable { var get: String? var parameters: [String]? = [] var errors: [String]? = [] var results: Int? var response: [String]? = [] // 不需要自定义init(from:),Codable会自动生成 init() {} }
内容的提问来源于stack exchange,提问作者Scott Carper
相关产品推荐
相关产品推荐

