Swift - JSONDecoder & Combine 框架问题

Posted

技术标签:

【中文标题】Swift - JSONDecoder & Combine 框架问题【英文标题】:Swift - JSONDecoder & Combine framework issue 【发布时间】:2021-07-01 20:00:39 【问题描述】:

我在视图中有一个搜索栏,我可以在其中进行搜索,搜索将被传递到 REST api,结果将显示在 tableView 上。以下是我的不同课程

型号:

struct MovieResponse: Codable 
    
    var totalResults: Int
    var response: String
    var error: String
    var movies: [Movie]
    
    enum ConfigKeys: String, CodingKey 
        case totalResults
        case response = "Response"
        case error = "Error"
        case movies
    
    
    init(totalResults: Int, response: String, error: String, movies: [Movie]) 
        self.totalResults = totalResults
        self.response = response
        self.error = error
        self.movies = movies
    
    
    init(from decoder: Decoder) throws 
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults) ?? 0
        self.response = try values.decodeIfPresent(String.self, forKey: .response) ?? "False"
        self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
        self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies) ?? []
    


extension MovieResponse 
    struct Movie: Codable, Identifiable 
        var id = UUID()
        var title: String
        var year: Int8
        var imdbID: String
        var type: String
        var poster: URL
        
        enum EncodingKeys: String, CodingKey 
            case title = "Title"
            case year = "Year"
            case imdmID
            case type = "Type"
            case poster = "Poster"
        
    

视图模型:

final class MovieListViewModel: ObservableObject 

    @Published var isLoading: Bool = false
    @Published var movieObj = MovieResponse(totalResults: 0, response: "False", error: "", movies: [])

    var searchTerm: String = ""

    private let searchTappedSubject = PassthroughSubject<Void, Error>()
    private var disposeBag = Set<AnyCancellable>()
    private let service = OMDBService()

    init() 
        searchTappedSubject
        .flatMap 
            self.requestMovies(searchTerm: self.searchTerm)
                .handleEvents(receiveSubscription:  _ in
                    DispatchQueue.main.async 
                        self.isLoading = true
                    
                ,
                receiveCompletion:  comp in
                    DispatchQueue.main.async 
                        self.isLoading = false
                    
                )
                .eraseToAnyPublisher()
        
        .replaceError(with: [])
        .receive(on: DispatchQueue.main)
        .assign(to: \.movieObj.movies, on: self)
        .store(in: &disposeBag)
    

    func onSearchTapped() 
        searchTappedSubject.send(())
    

    private func requestMovies(searchTerm: String) -> AnyPublisher<[MovieResponse.Movie], Error> 
        guard let url = URL(string:"\(Constants.HostName)/?s=\(searchTerm)&apikey=\(Constants.APIKey)") else 
            fatalError("Something is wrong with URL")
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .tryMap()  element -> Data in
                    guard let httpResponse = element.response as? HTTPURLResponse,
                        httpResponse.statusCode == 200 else 
                            throw URLError(.badServerResponse)
                        
                    return element.data
                    
               .mapError  $0 as Error 
            .decode(type: [MovieResponse.Movie].self, decoder: JSONDecoder())
               .eraseToAnyPublisher()
    

最后是视图和搜索栏

struct SearchView: View 

    @ObservedObject var viewModel = MovieListViewModel()
    
    @State private var searchText = ""
    
    var body: some View 
        ZStack 
            VStack 
                HStack 
                    Text("Search OMDB")
                        .font(.system(size: 25, weight: .black, design: .rounded))
                    Spacer()
                
                .padding()
                Spacer()
                SearchBar(text: $viewModel.searchTerm,
                            onSearchButtonClicked: viewModel.onSearchTapped)
                List(viewModel.movieObj.movies)  movie in
                    Text(verbatim: movie.title)
                
                .onAppear() 
                    print("Got the new data")
                
            
        
    


struct SearchBar: UIViewRepresentable 

    @Binding var text: String
    var onSearchButtonClicked: (() -> Void)? = nil

    class Coordinator: NSObject, UISearchBarDelegate 

        let control: SearchBar

        init(_ control: SearchBar) 
            self.control = control
        

        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) 
            control.text = searchText
        

        func searchBarSearchButtonClicked(_ searchBar: UISearchBar) 
            control.onSearchButtonClicked?()
        
    

    func makeCoordinator() -> Coordinator 
        return Coordinator(self)
    

    func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar 
        let searchBar = UISearchBar(frame: .zero)
        searchBar.delegate = context.coordinator
        return searchBar
    
    func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) 
        uiView.text = text
    

当我运行代码时,REST api 正在返回数据,但我在 Movie 数组中看不到相同的数据,而 List 没有显示任何内容。

编辑: 添加 REST API 返回的示例 json


     "Search": [
         
             "Title": "What We Do in the Shadows",
             "Year": "2014",
             "imdbID": "tt3416742",
             "Type": "movie",
             "Poster": "https://m.media-amazon.com/images/M/MV5BMjAwNDA5NzEwM15BMl5BanBnXkFtZTgwMTA1MDUyNDE@._V1_SX300.jpg"
         ,
         
             "Title": "I Know What You Did Last Summer",
             "Year": "1997",
             "imdbID": "tt0119345",
             "Type": "movie",
             "Poster": "https://m.media-amazon.com/images/M/MV5BZDI4ODJlNGUtNjFiMy00ODgzLWIzYjgtMzgyZTljZDQ2NGZiXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg"
         
     ],
     "totalResults": "4365",
     "Response": "True"
 

【问题讨论】:

您使用的是.replaceError(with: []),因此您实际上可能看不到错误。您确定在此之前的某个地方没有发生错误吗? 运行时无任何错误发生。我怀疑它是一个解码问题,但无法找到确切的问题。 对——如果是解码错误,我建议您实际捕获该错误并做出响应,而不是仅仅将错误替换为[] 我认为问题在于movieObj 是@Published,而不是电影(应该触发列表重新加载)。 @Published var movieObj = MovieResponse(totalResults: 0, response: "False", error: "", movies: []) 如果你有 @Published var movies = [],并在收到响应后设置它,它应该可以工作. 【参考方案1】:

您的代码几乎没有问题。

    主要是您尝试解码的类型不正确。数据是一个字典,可以解码为MovieResponse,但您正试图解码为[MovieResponse.Movie]。这将失败。

您可以通过添加轻松找到解码/网络错误

.mapError( (error) -> Error in
  print("error -- \(error)")
  return error
)

修复此更改

.decode(type: [MovieResponse.Movie].self, decoder: JSONDecoder())

.decode(type: MovieResponse.self, decoder: JSONDecoder())
.map(\.movies)

还有一些与编码键不匹配的问题, 对于MovieResponse

    将拼写错误 ConfigKeys 修复为 CodingKeys movies 数组应该从 Search 对象解码 totalResults 应该是一个字符串。

对于Movie

    EncodingKeys 更改为CodingKeys 将拼写错误 imdmID 修复为 imdbIDyear 的类型更改为String 或使用自定义解码 这些更改将解决映射问题。

提示:至少在发布问题之前检查您的拼写错误;)

【讨论】:

虽然@Johnykutty 是正确的,但我也建议您在掌握编写解码器的窍门之前稍微作弊。这可能非常令人生畏。首先,我将通过 JSON Formatter 运行 JSON 响应以清理它,然后通过 Quicktype 运行它,这将为您编写一个解码器。解码器会笨重且丑陋,但它会起作用,您可以了解如何使用您关心的数据为自己构建解码器。然后,您可以学习编写更好、更简洁的解码器。 Quicktype 很棒。 我已按照建议执行了所有操作,但仍然无法看到列表。我想我放弃了CombinePassthroughSubject 的想法 我尝试使用您的代码,但结果是我必须按 Enter 才能显示列表。 Quicktype 是一个开始,而不是结束。根据您用于创建解码器的返回数据,您可以获得一些奇怪且不必要的变量。此外,@Johnykutty 是对的,您设置搜索器的方式不会导致列表自动出现。它会等到您按 Enter 键开始搜索。否则,输入的每个字符都会导致重新搜索数据库。有办法解决这个问题,但这超出了你现在想做的事情。

以上是关于Swift - JSONDecoder & Combine 框架问题的主要内容,如果未能解决你的问题,请参考以下文章

JSONDecoder() 仅在 Swift 中处理 Null 值

Swift - 将类型动态传递给 JSONDecoder

如何在 swift 中使用 JSONDecoder 输入调整?

Swift 5:如何从 JSONDecoder().decode 获取数据?

Swift - 使用 JSONDecoder 解码 JSON 数据

Swift 的 JSONDecoder 在 JSON 字符串中有多种日期格式?