对 Unsplash api 的异步请求无法正常工作

async request to Unsplash api not working correctly

我的 swift 包 UnsplashSwiftUI 遇到了一些问题 在 WWDC 之前,我遇到了一些问题导致我的视​​图重新加载(正如您在主分支上看到的那样)但是当 async/await 宣布时,这似乎是我的包的绝佳机会。

我正在开发分支上 async/await 开发包。

但是,我现在在处理异步 API 请求时遇到了一些问题。

这是我的最小可重现示例,我从我的异步函数 getURL() 的 catch 块中得到打印错误 'Failed to fetch image'。我还尝试在内部使用异步调用任务。

//From this
.task {
    await getURL()
}

//To this
.task {
    async {
        await getURL()
    }
}

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    var body: some View {
        VStack {
            UnsplashRandom(clientId: "TSozaArCYtCWcXnnUkh4KvKJ5ZfmVOn_FYbIVVn76Ew")
                .frame(width: 500, height: 500)
        }
    }
}
PlaygroundPage.current.setLiveView(ContentView())
import SwiftUI

@available(iOS 15, OSX 12, *)
public struct UnsplashRandom: View {
    
    //MARK: Parameters
    //Required parameters
    var clientId: String //Unsplash API access key
    
    @State private var unsplashData: UnsplashData? = nil
    @State private var requestURL: URL? = nil
    
    //MARK: Init
    public init(clientId: String) {
        self.clientId = clientId
        
        let url = URL(string: "https://api.unsplash.com/")!
        
        guard var components = URLComponents(url: url.appendingPathComponent("photos/random"), resolvingAgainstBaseURL: true)
        else { fatalError("Couldn't append path component")}

        components.queryItems = [URLQueryItem(name: "client_id", value: clientId)]
        
        _requestURL = State(initialValue: components.url!)
    }
    
    //MARK: Body
    public var body: some View {
        //MARK: Main View
        ZStack(alignment: .bottomTrailing) {
            //MARK: Remote Image
            AsyncImage (url: URL(string: unsplashData?.urls!.raw! ?? "https://images.unsplash.com/photo-1626643590239-4d5051bafbcc?ixid=MnwxOTUzMTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE2MjY5Njc0MjI&ixlib=rb-1.2.1")!)
                .aspectRatio(contentMode: .fit)
        }
        .task {
            await getURL()
        }
    }
    
    func getURL() async {
        do {
            let (data, _) = try await URLSession.shared.data(from: requestURL!)
            unsplashData =  try JSONDecoder().decode(UnsplashData.self, from: data)
        } catch {
            print("Failed to fetch image")
        }
    }
}
import Foundation

// MARK: - UnsplashData
struct UnsplashData: Codable {
    let id: String?
    let createdAt, updatedAt, promotedAt: Date?
    let width, height: Int?
    let color, blurHash: String?
    let unsplashDataDescription: String?
    let altDescription: String?
    let urls: Urls?
    let links: UnsplashDataLinks?
    let categories: [String]?
    let likes: Int?
    let likedByUser: Bool?
    let currentUserCollections: [String]?
    let sponsorship: JSONNull?
    let user: User?
    let exif: Exif?
    let location: Location?
    let views, downloads: Int?

    enum CodingKeys: String, CodingKey {
        case id
        case createdAt = "created_at"
        case updatedAt = "updated_at"
        case promotedAt = "promoted_at"
        case width, height, color
        case blurHash = "blur_hash"
        case unsplashDataDescription = "description"
        case altDescription = "alt_description"
        case urls, links, categories, likes
        case likedByUser = "liked_by_user"
        case currentUserCollections = "current_user_collections"
        case sponsorship, user, exif, location, views, downloads
    }
}

// MARK: - Exif
struct Exif: Codable {
    let make, model, exposureTime, aperture: String?
    let focalLength: String?
    let iso: Int?

    enum CodingKeys: String, CodingKey {
        case make, model
        case exposureTime = "exposure_time"
        case aperture
        case focalLength = "focal_length"
        case iso
    }
}

// MARK: - UnsplashDataLinks
struct UnsplashDataLinks: Codable {
    let linksSelf, html, download, downloadLocation: String?

    enum CodingKeys: String, CodingKey {
        case linksSelf = "self"
        case html, download
        case downloadLocation = "download_location"
    }
}

// MARK: - Location
struct Location: Codable {
    let title, name, city, country: String?
    let position: Position?
}

// MARK: - Position
struct Position: Codable {
    let latitude, longitude: Double?
}

// MARK: - Urls
struct Urls: Codable {
    let raw, full, regular, small: String?
    let thumb: String?
}

// MARK: - User
struct User: Codable {
    let id: String?
    let updatedAt: Date?
    let username, name, firstName, lastName: String?
    let twitterUsername: String?
    let portfolioURL: String?
    let bio: String?
    let location: String?
    let links: UserLinks?
    let profileImage: ProfileImage?
    let instagramUsername: String?
    let totalCollections, totalLikes, totalPhotos: Int?
    let acceptedTos: Bool?

    enum CodingKeys: String, CodingKey {
        case id
        case updatedAt = "updated_at"
        case username, name
        case firstName = "first_name"
        case lastName = "last_name"
        case twitterUsername = "twitter_username"
        case portfolioURL = "portfolio_url"
        case bio, location, links
        case profileImage = "profile_image"
        case instagramUsername = "instagram_username"
        case totalCollections = "total_collections"
        case totalLikes = "total_likes"
        case totalPhotos = "total_photos"
        case acceptedTos = "accepted_tos"
    }
}

// MARK: - UserLinks
struct UserLinks: Codable {
    let linksSelf, html, photos, likes: String?
    let portfolio, following, followers: String?

    enum CodingKeys: String, CodingKey {
        case linksSelf = "self"
        case html, photos, likes, portfolio, following, followers
    }
}

// MARK: - ProfileImage
struct ProfileImage: Codable {
    let small, medium, large: String?
}

// MARK: - Encode/decode helpers

class JSONNull: Codable, Hashable {

    public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
        return true
    }

    public var hashValue: Int {
        return 0
    }

    public func hash(into hasher: inout Hasher) {
        // No-op
    }

    public init() {}

    public required init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if !container.decodeNil() {
            throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encodeNil()
    }
}

在您的模型中,UnsplashData 和 User 替换 Date?用字符串?

之后,这是我测试答案的方式:

import SwiftUI

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
@State var unsplashData: UnsplashData?

var body: some View {
    VStack {
        if let unsplash = unsplashData {
            Text("user is \(unsplash.user?.name ?? "no name")")
        } else {
            Text("testing testing")
        }
    }
    .task {
        await getUnsplashData()
    }
}

func getUnsplashData() async {
    let fetchResponse: UnsplashData? = await fetchIt()
    if let theResponse = fetchResponse {
        self.unsplashData = theResponse
        print("\n-----> getUnsplashData: \(theResponse)")
    }
}
    
    func fetchIt<T: Decodable>() async -> T? {
        let url = URL(string: "https://api.unsplash.com/photos/random?client_id=TSozaArCYtCWcXnnUkh4KvKJ5ZfmVOn_FYbIVVn76Ew")!
        let request = URLRequest(url: url)
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                // throw URLError(.badServerResponse)   //  todo
                print(URLError(.badServerResponse))
                return nil
            }
            let results = try JSONDecoder().decode(T.self, from: data)
            return results
        }
        catch {
            return nil
        }
    }
 
}