【问题标题】:Couldn't not fetching the data from Spotify API with Combine framework in SwiftUI无法使用 SwiftUI 中的组合框架从 Spotify API 获取数据
【发布时间】:2026-02-04 07:10:01
【问题描述】:

这是我的想法,我想在用户通过$artistName搜索时呈现数据,然后组合框架可以帮助我从服务器请求数据。

我不知道我错了哪一步。当我尝试通过模拟器获取数据时。它将显示未知错误和其他错误。

2020-10-23 03:56:38.220523+0800 PodcastSearchV2[42967:667554] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
Fetch failed: Unknown error
2020-10-23 03:56:40.114906+0800 PodcastSearchV2[42967:667546] [connection] nw_connection_copy_protocol_metadata [C2] Client called nw_connection_copy_protocol_metadata on unconnected nw_connection
2020-10-23 03:56:40.115071+0800 PodcastSearchV2[42967:667546] [connection] nw_connection_copy_protocol_metadata [C2] Client called nw_connection_copy_protocol_metadata on unconnected nw_connection
2020-10-23 03:56:40.115255+0800 PodcastSearchV2[42967:667546] [connection] nw_connection_copy_protocol_metadata [C2] Client called nw_connection_copy_protocol_metadata on unconnected nw_connection
2020-10-23 03:56:40.115360+0800 PodcastSearchV2[42967:667546] [connection] nw_connection_copy_protocol_metadata [C2] Client called nw_connection_copy_protocol_metadata on unconnected nw_connection
2020-10-23 03:56:40.115516+0800 PodcastSearchV2[42967:667546] [connection] nw_connection_copy_connected_local_endpoint [C2] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection
2020-10-23 03:56:40.115613+0800 PodcastSearchV2[42967:667546] [connection] nw_connection_copy_connected_remote_endpoint [C2] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection
2020-10-23 03:56:40.115723+0800 PodcastSearchV2[42967:667546] [connection] nw_connection_copy_connected_path [C2] Client called nw_connection_copy_connected_path on unconnected nw_connection
2020-10-23 03:56:40.115924+0800 PodcastSearchV2[42967:667546] Connection 2: unable to determine interface type without an established connection
2020-10-23 03:56:40.116069+0800 PodcastSearchV2[42967:667546] Connection 2: unable to determine interface classification without an established connection
2020-10-23 03:56:40.116381+0800 PodcastSearchV2[42967:667546] [connection] nw_connection_copy_protocol_metadata [C2] Client called nw_connection_copy_protocol_metadata on unconnected nw_connection
2020-10-23 03:56:40.120441+0800 PodcastSearchV2[42967:667546] [connection] nw_connection_copy_metadata [C2] Client called nw_connection_copy_metadata on unconnected nw_connection
2020-10-23 03:56:40.120579+0800 PodcastSearchV2[42967:667546] Connection 2: unable to determine interface type without an established connection
2020-10-23 03:56:40.121823+0800 PodcastSearchV2[42967:667546] Connection 2: unable to determine interface type without an established connection

这是我的回应。

struct DataResponseSpotify: Codable {
    var episodes: PodcastItemSpotify
}

struct PodcastItemSpotify: Codable {
    var items: [PodcastItemsDetailsSpotify]
}

struct PodcastItemsDetailsSpotify: Codable, Identifiable {
    let id: String
    let description: String
    let images: [Images]
    let name: String
    let external: String
    
    var externalURL: URL? {
        return URL(string: external)
    }
    
    enum CodingKeys: String, CodingKey {
        case id
        case description
        case images
        case name
        case external = "external_urls"
    }
}

struct Images: Codable {
    let url: String
    
    var imageURL: URL? {
        return URL(string: url)
    }
    
    enum CodingKeys: String, CodingKey {
        case url
    }
}

struct Token: Codable {
    let accessToken: String
    
    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
    }
}

这就是我在模型中的写法。

class DataObserverSpotify: ObservableObject {
    
    @Published var artistName = ""
    @Published var token = ""
    @Published var fetchResult = [PodcastItemsDetailsSpotify]()
    var subscriptions: Set<AnyCancellable> = []
    let podsURLComponents = PodsFetcher()
    
    init() {
        getToken()
        $artistName
            .debounce(for: .seconds(2), scheduler: RunLoop.main)
            .removeDuplicates()
            .compactMap { query in
                let url = self.podsURLComponents.makeURLComponentsForSpotify(withName: query, tokenAccess: self.token)
                return URL(string: url.string ?? "")
            }
            .flatMap(fetchDatatesting)
            .receive(on: DispatchQueue.main)
            .assign(to: \.fetchResult, on: self)
            .store(in: &subscriptions)
    }
    
     
    func fetchDatatesting(url: URL) -> AnyPublisher<[PodcastItemsDetailsSpotify], Never> {
        URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: DataResponseSpotify.self, decoder: JSONDecoder())
            .map(\.episodes.items)
            .replaceError(with: [])
            .eraseToAnyPublisher()
    }
    
    
    func getToken() {
        let parameters = "grant_type=refresh_token&refresh_token=[example-refresh-token]"
        let postData =  parameters.data(using: .utf8)
        var request = URLRequest(url: URL(string: "https://accounts.spotify.com/api/token")!,timeoutInterval: Double.infinity)
        request.addValue("Basic exampleBasicAuth=", forHTTPHeaderField: "Authorization")
        request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.addValue("inapptestgroup=; __Host-device_id=example_id; __Secure-example=; csrf_token=example", forHTTPHeaderField: "Cookie")

        request.httpMethod = "POST"
        request.httpBody = postData

        URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data {
                let decoder = JSONDecoder()
                decoder.dateDecodingStrategy = .iso8601
                if let token = try? decoder.decode(Token.self, from: data) {
                    DispatchQueue.main.async {
                        self.token = token.accessToken
                    }
                    return
                }
            }
            print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
        }.resume()
    }
    

}

【问题讨论】:

    标签: api swiftui spotify combine


    【解决方案1】:

    您调用getToken 设置令牌异步,但无需等待,您立即尝试创建一个URL,假设令牌在.compactMap 中可用 - 到那时它还不可用.

    因此,您必须“等待”它,而使用 Combine,您可以通过多种方式做到这一点。

    与您当前所拥有的最简单(最便宜)的更改是将$token$artistName 发布者结合起来:

    $artistName
        .debounce(for: .seconds(2), scheduler: RunLoop.main)
        .removeDuplicates()
        .combineLatest($token.filter { !$0.isEmpty }) // waits for a non-empty token
        .compactMap { (query, token) in
           var url = ... // construct URL from query and token
           return url
        }
        .flatMap(fetchDatatesting)
        .receive(on: DispatchQueue.main)
        // I used sink, since assign causes a self retain cycle
        .sink { [weak self] result in self?.fetchResult = result }
        .store(in: &subscriptions)
          
    

    但是你真的应该重新设计你的 getToken 方法 - 它写得不好,因为它是一个异步函数,但它没有完成处理程序。

    例如,像这样:

    func getToken(_ completion: @escaping (Token) -> Void) {
       // set-up, etc..   
       URLSession.shared.dataTask(with: request) { data, response, error in
          // error handling, etc...
          if let token = try? decoder.decode(Token.self, from: data) {
              completion(token)
          }
    
       }.resume()
    }
    
    // usage
    getToken { 
       self.token = $0
    }
    

    或者,由于您使用的是 Combine,您可以重新编写它以返回发布者(我在这里忽略错误以保持简洁):

    func getToken() -> AnyPublisher<Token?, Never> {
       // ...
    }
    

    【讨论】:

    • 谢谢,你帮了我很多。我要去研究一下。