使用 SwiftUI 中的 Google Books API 从 ISBN 异步获取图书信息

Asynchronously fetching book information from ISBN using the Google Books API in SwiftUI

我正在尝试从其 ISBN 获取一本书的详细信息。这是我到目前为止所拥有的一个可重现的例子。我希望在按下按钮时获取数据,但是我所拥有的不起作用。我还将包括以下请求的数据模型。此外,我想在获取数据时叠加某种加载动画。

struct ContentView: View {
    @State var name: String = ""
    @State var author: String = ""
    @State var total: String = ""
    
    @State var code = "ISBN"
    
    private func fetchBook(id identifier: String) async throws -> GoogleBook {
        let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q={\(identifier)}")
        let (data, _) = try await URLSession.shared.data(from: url!)
        return try! JSONDecoder().decode(GoogleBook.self, from: data)
    }
    
    var body: some View {
        VStack {
            Text("Name: \(name)")
            Text("Author: \(author)")
            Text("total: \(total)")

            Button(action: {
                code = "9780141375632"
                Task {
                    do {
                        let fetchedBooks = try await fetchBook(id: code)
                        let book = fetchedBooks.items[0].volumeInfo
                        name = book.title
                        author = book.authors[0]
                        total = String(book.pageCount!)
                    } catch {
                        print(error.localizedDescription)
                    }
                }
            }, label: {
                Rectangle()
                    .frame(width: 200, height: 100)
                    .foregroundColor(.blue)
        })
        }
    }
}
import Foundation

// MARK: - GoogleBook
struct GoogleBook: Decodable {
    let kind: String
    let totalItems: Int
    let items: [Item]
}

// MARK: - Item
struct Item: Decodable {
    let kind: Kind
    let id, etag: String
    let selfLink: String
    let volumeInfo: VolumeInfo
    let saleInfo: SaleInfo
    let accessInfo: AccessInfo
    let searchInfo: SearchInfo
}

// MARK: - AccessInfo
struct AccessInfo: Decodable {
    let country: Country
    let viewability: Viewability
    let embeddable, publicDomain: Bool
    let textToSpeechPermission: TextToSpeechPermission
    let epub, pdf: Epub
    let webReaderLink: String
    let accessViewStatus: AccessViewStatus
    let quoteSharingAllowed: Bool
}

enum AccessViewStatus: String, Decodable {
    case none = "NONE"
    case sample = "SAMPLE"
}

enum Country: String, Decodable {
    case countryIN = "IN"
}

// MARK: - Epub
struct Epub: Decodable {
    let isAvailable: Bool
    let acsTokenLink: String?
}

enum TextToSpeechPermission: String, Decodable {
    case allowed = "ALLOWED"
    case allowedForAccessibility = "ALLOWED_FOR_ACCESSIBILITY"
}

enum Viewability: String, Decodable {
    case noPages = "NO_PAGES"
    case partial = "PARTIAL"
}

enum Kind: String, Decodable {
    case booksVolume = "books#volume"
}

// MARK: - SaleInfo
struct SaleInfo: Decodable {
    let country: Country
    let saleability: Saleability
    let isEbook: Bool
    let listPrice, retailPrice: SaleInfoListPrice?
    let buyLink: String?
    let offers: [Offer]?
}

// MARK: - SaleInfoListPrice
struct SaleInfoListPrice: Decodable {
    let amount: Double
    let currencyCode: CurrencyCode
}

enum CurrencyCode: String, Decodable {
    case inr = "INR"
}

// MARK: - Offer
struct Offer: Decodable {
    let finskyOfferType: Int
    let listPrice, retailPrice: OfferListPrice
}

// MARK: - OfferListPrice
struct OfferListPrice: Decodable {
    let amountInMicros: Int
    let currencyCode: CurrencyCode
}

enum Saleability: String, Decodable {
    case forSale = "FOR_SALE"
    case notForSale = "NOT_FOR_SALE"
}

// MARK: - SearchInfo
struct SearchInfo: Decodable {
    let textSnippet: String
}

// MARK: - VolumeInfo
struct VolumeInfo: Decodable {
    let title: String
    let authors: [String]
    let publisher, publishedDate, volumeInfoDescription: String
    let industryIdentifiers: [IndustryIdentifier]
    let readingModes: ReadingModes
    let pageCount: Int?
    let printType: PrintType
    let categories: [String]?
    let averageRating: Double?
    let ratingsCount: Int?
    let maturityRating: MaturityRating
    let allowAnonLogging: Bool
    let contentVersion: String
    let panelizationSummary: PanelizationSummary?
    let imageLinks: ImageLinks
    let language: Language
    let previewLink: String
    let infoLink: String
    let canonicalVolumeLink: String
    let subtitle: String?
    let comicsContent: Bool?
    let seriesInfo: SeriesInfo?

    enum CodingKeys: String, CodingKey {
        case title, authors, publisher, publishedDate
        case volumeInfoDescription = "description"
        case industryIdentifiers, readingModes, pageCount, printType, categories, averageRating, ratingsCount, maturityRating, allowAnonLogging, contentVersion, panelizationSummary, imageLinks, language, previewLink, infoLink, canonicalVolumeLink, subtitle, comicsContent, seriesInfo
    }
}

// MARK: - ImageLinks
struct ImageLinks: Decodable {
    let smallThumbnail, thumbnail: String
}

// MARK: - IndustryIdentifier
struct IndustryIdentifier: Decodable {
    let type: TypeEnum
    let identifier: String
}

enum TypeEnum: String, Decodable {
    case isbn10 = "ISBN_10"
    case isbn13 = "ISBN_13"
}

enum Language: String, Decodable {
    case en = "en"
}

enum MaturityRating: String, Decodable {
    case notMature = "NOT_MATURE"
}

// MARK: - PanelizationSummary
struct PanelizationSummary: Decodable {
    let containsEpubBubbles, containsImageBubbles: Bool
    let imageBubbleVersion: String?
}

enum PrintType: String, Decodable {
    case book = "BOOK"
}

// MARK: - ReadingModes
struct ReadingModes: Decodable {
    let text, image: Bool
}

// MARK: - SeriesInfo
struct SeriesInfo: Decodable {
    let kind, shortSeriesBookTitle, bookDisplayNumber: String
    let volumeSeries: [VolumeSery]
}

// MARK: - VolumeSery
struct VolumeSery: Decodable {
    let seriesID, seriesBookType: String
    let orderNumber: Int
    let issue: [Issue]

    enum CodingKeys: String, CodingKey {
        case seriesID = "seriesId"
        case seriesBookType, orderNumber, issue
    }
}

// MARK: - Issue
struct Issue: Decodable {
    let issueDisplayNumber: String
}

有几个问题。

  • 永远不要在 throws 的方法中 try! 提交错误。
  • 永远不要在解码上下文中打印 error.localizedDescription,始终只打印 error 实例。
  • 永远不要强制展开由字符串插值组成的 URLs,失败时抛出错误。

主要问题是您必须通过添加百分比编码

来对 URL 进行编码
private func fetchBook(id identifier: String) async throws -> GoogleBook {
    guard let encodedString = "https://www.googleapis.com/books/v1/volumes?q={\(identifier)}"
                              .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
          let url = URL(string: encodedString) else { throw URLError(.badURL)}
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(GoogleBook.self, from: data)
}

如果您将遇到任何 DecodingError,错误消息会告诉您它发生的确切原因和位置

要显示进度视图,请添加一个视图模型,其中 @Published 属性 代表一个状态,一个具有关联值的枚举,例如这个通用枚举

enum LoadingState<Value> {
    case loading(Double)
    case loaded(Value)
}

关联的Double值可以传递进度百分比。