首页 > 解决方案 > 与 API 交互时的 keyNotFound CodingKeys

问题描述

我正在使用 TMDB api,并且正在构建一个网络层来执行此操作。我也在关注一个教程,我很确定我正确地遵循了。我正在调用这个 API 端点:

{"page":1,
"results":[{"adult":false,"backdrop_path":"/fev8UFNFFYsD5q7AcYS8LyTzqwl.jpg","genre_ids":[16,28,35,10751],
"id":587807,
"original_language":"en","original_title":"Tom & Jerry",
"overview":"Jerry moves into New York City's finest hotel on the eve of the wedding of the century, forcing the desperate event planner to hire Tom to get rid of him. As mayhem ensues, the escalating cat-and-mouse battle soon threatens to destroy her career, the wedding, and possibly the hotel itself.",
"popularity":4136.493,
"poster_path":"/6KErczPBROQty7QoIsaa6wJYXZi.jpg",
"release_date":"2021-02-12",
"title":"Tom & Jerry",
"video":false,
"vote_average":8,
"vote_count":541}]
}

不幸的是,我收到了这个错误: keyNotFound(CodingKeys(stringValue: "backdrop_path", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "results", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"backdrop_path\", intValue: nil) (\"backdrop_path\").", underlyingError: nil))

这适用于我的Resulting结构中的每个键。

这是我的 ViewController 中的方法:

func fetchUpComing() {
        let api = MovieDB.api
        api.send(request: .upcomingMovies(completion: { result in
            print("result", result)
            switch result {
            case .success(let page):
              print(page.results)
                self.upcomingMovies = page.results
              var basicSection = MovieSection()
              basicSection.numberOfItems = self.upcomingMovies.count
              basicSection.upcomingItems = page.results
              self.sections = [TitleSection(title: "Upcoming Movies"), basicSection]
              self.setupCollectionView()
            case .failure(let error):  print(error)
            }
        }))
    }

电影数据库:

struct MovieDB { // logic specific to the TMDB API
    public static let baseURL = URL(string: "https://api.themoviedb.org/3/")!
    public static var api: APIClient = {
        let configuration = URLSessionConfiguration.default
        let apiKey = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJmMzM2Yzk4MmY0MzdhMGQ1OTQ1MDFlY2U1YTA0ZmI3NiIsInN1YiI6IjYwM2ZjMTg5OTdlYWI0MDA3NzViYWIyMCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.J5hW42SMtm7RBeVGy9Z_Ex"
        configuration.httpAdditionalHeaders = [
            "Authorization": "Bearer"
        ]
        return APIClient(configuration: configuration)
    }()
    public static let baseImageURL = URL(string: "https://image.tmdb.org/t/p/w500")!
}

public extension Result where Success == Data, Failure == Error {
    func decoding<M: Model>(_ model: M.Type, completion: @escaping (Result<M, Error>) -> Void) {
      // decode the JSON in the background and call the completion block on the main thread
      DispatchQueue.global().async {
            //Result’s flatMap() method takes the successful case (if it was successful) and applies your block. You can return a new Result that contains a successful value or an error.
            let result = self.flatMap { data -> Result<M, Error> in
                do {
                    let decoder = M.decoder
                    let model = try decoder.decode(M.self, from: data)
                    return .success(model)
                } catch {
                    print("Error: \(error) with :\(String(data: data, encoding: .utf8))")
                    return .failure(error)
                }
            }
            DispatchQueue.main.async {
                completion(result)
            }
        }
    }

API客户端:

    private let session: URLSession

    init(configuration: URLSessionConfiguration) {
        session = URLSession(configuration: configuration)
    }
    
    public func send(request: Request) {
        let urlRequest = request.builder.toURLRequest()
        let task = session.dataTask(with: urlRequest) { data, response, error in
            let result: Result<Data, Error>
            if let error = error {
                result = .failure(error)
            } else {
                result = .success(data ?? Data())
            }
            DispatchQueue.main.async {
                request.completion(result)
            }
        }
        task.resume()
    }
}

要求:

public struct Request {
    let builder: RequestBuilder
    let completion: (Result<Data, Error>) -> Void

    init(builder: RequestBuilder, completion: @escaping (Result<Data, Error>) -> Void) {
        self.builder = builder
        self.completion = completion
    }
    
    public static func basic(method: HTTPMethod = .get, baseURL: URL, path: String, params: [URLQueryItem]? = nil, completion: @escaping (Result<Data, Error>) -> Void) -> Request {
        let builder = BasicRequestBuilder(method: method, baseURL: baseURL, path: path, params: params)
        return Request(builder: builder, completion: completion)
    }
}

extension Request {
    static func popularMovies(completion: @escaping (Result<PagedResults<Movie>, Error>) -> Void) -> Request {
        Request.basic(baseURL: MovieDB.baseURL, path: "discover/movie", params: [
            URLQueryItem(name: "sort_by", value: "popularity.desc")
        ]) { result in
            result.decoding(PagedResults<Movie>.self, completion: completion)        }
    }
    static func upcomingMovies(completion: @escaping (Result<Upcoming<Resulting>, Error>) -> Void) -> Request {
        Request.basic(baseURL: MovieDB.baseURL, path: "movie/upcoming", params: [
            URLQueryItem(name: "sort_by", value: "popularity.desc")
        ]) { result in
            result.decoding(Upcoming<Resulting>.self, completion: completion)        }
    }
}

请求生成器:

public enum HTTPMethod: String {
    case get
    case post
    case put
    case delete
}

public protocol RequestBuilder {
    var method: HTTPMethod { get }
    var baseURL: URL { get }
    var path: String { get }
    var params: [URLQueryItem]? { get }
    var headers: [String: String] { get }

    func toURLRequest() -> URLRequest
}

public extension RequestBuilder {
    func toURLRequest() -> URLRequest {
        var components = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: false)!
        components.queryItems = params
        let url = components.url!

        var request = URLRequest(url: url)
        request.allHTTPHeaderFields = headers
        request.httpMethod = method.rawValue.uppercased()
        return request
    }
}

struct BasicRequestBuilder: RequestBuilder {
    var method: HTTPMethod
    var baseURL: URL
    var path: String
    var params: [URLQueryItem]?
    var headers: [String: String] = [:]
}

这是我在解码中使用的结构:

import Foundation

struct Movie: Model, Hashable {
    let id: Int
    let title: String
    let posterPath: String
    let releaseDate: String
}


struct PagedResults<T: Model>: Model {
    let page: Int
    let totalPages: Int
    let results: [T]
}

extension PagedResults {
    static var decoder: JSONDecoder { T.decoder }
}

struct Upcoming<T: Model>: Model {
    
    let dates: Dates
    let page: Int
    let results: [Resulting]
    let totalPages, totalResults: Int

    enum CodingKeys: String, CodingKey {
        case dates, page, results
        case totalPages = "total_pages"
        case totalResults = "total_results"
    }
}

extension Upcoming {
    static var decoder: JSONDecoder { T.decoder }
}

// MARK: - Dates
struct Dates: Codable {
    let maximum, minimum: String
}

// MARK: - Result
struct Resulting: Model, Hashable{
    let adult: Bool
    let backdropPath: String
    let genreIDS: [Int]
    let id: Int
    let originalLanguage: OriginalLanguage
    let originalTitle, overview: String
    let popularity: Double
    let posterPath, releaseDate, title: String
    let video: Bool
    let voteAverage: Double
    let voteCount: Int

    enum CodingKeys: String, CodingKey {
        case adult
        case backdropPath = "backdrop_path"
        case genreIDS = "genre_ids"
        case id
        case originalLanguage = "original_language"
        case originalTitle = "original_title"
        case overview, popularity
        case posterPath = "poster_path"
        case releaseDate = "release_date"
        case title, video
        case voteAverage = "vote_average"
        case voteCount = "vote_count"
    }
  
}

enum OriginalLanguage: String, Codable {
    case en = "en"
    case es = "es"
    case ja = "ja"
}

标签: swiftapistruct

解决方案


推荐阅读