When a user pastes a hexcode into my textfield, I want my API function to take this hexcode and use it as a parameter in the API call. This means I have to share data from my View (containing the textfield) to my Class (containing API call). What is the best way to go about this? Appreciate the time and advice 🙏
View:
import SwiftUI
struct TestingText: View {
#StateObject var fetch = fetchResults()
#Binding var text: String
var body: some View {
VStack {
TextField("Paste Clout Hexcode Here", text: $text)
.font(.title2)
.padding()
Text(fetch.clout.postFound?.body ?? "n/a")
}
}
}
struct TestingText_Previews: PreviewProvider {
static var previews: some View {
TestingText(text: .constant("8004bb672ad3f46118775cd4b2cb5306c63f6d68787457990bf0d2fda3f7993a"))
}
}
Class with API call:
class fetchResults: ObservableObject {
#Published var clout = Cloutington()
#Published var dataHasLoaded = false
#State var postHashHex: String = "8004bb672ad3f46118775cd4b2cb5306c63f6d68787457990bf0d2fda3f7993a"
init() {
getData { clout in
self.clout = clout
}
}
private func getData(completion: #escaping (Cloutington) -> ()) {
let parameters = "{\r\n \"PostHashHex\": \"\(postHashHex)\"\r\n}"
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://bitclout.com/api/v0/get-single-post")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = postData
request.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: request) { (responseData, response, error) in
print(error)
print(response)
print(responseData)
if let resData = responseData {
let decoder = JSONDecoder()
do
{
let finalData = try decoder.decode(Cloutington.self, from: resData)
DispatchQueue.main.async {
completion(finalData)
self.dataHasLoaded = true
}
}
catch (let error)
{
print(error)
}
}
}
task.resume()
}
}
Model with JSON data:
import Foundation
struct Cloutington: Decodable {
var postFound: PostFound?
enum CodingKeys: String, CodingKey {
case postFound = "PostFound"
}
}
struct PostFound: Decodable {
var id: String?
var postHashHex, posterPublicKeyBase58Check, parentStakeID, body: String?
var imageURLs: [String]?
// var recloutedPostEntryResponse: JSONNull?
var creatorBasisPoints, stakeMultipleBasisPoints: Int?
var timestampNanos: Double?
var isHidden: Bool?
var confirmationBlockHeight: Int?
var inMempool: Bool?
var profileEntryResponse: ProfileEntryResponse?
var likeCount, diamondCount: Int?
var isPinned: Bool?
var commentCount, recloutCount: Int?
var diamondsFromSender: Int?
enum CodingKeys: String, CodingKey {
case postHashHex = "PostHashHex"
case posterPublicKeyBase58Check = "PosterPublicKeyBase58Check"
case parentStakeID = "ParentStakeID"
case body = "Body"
case imageURLs = "ImageURLs"
case creatorBasisPoints = "CreatorBasisPoints"
case stakeMultipleBasisPoints = "StakeMultipleBasisPoints"
case timestampNanos = "TimestampNanos"
case isHidden = "IsHidden"
case confirmationBlockHeight = "ConfirmationBlockHeight"
case inMempool = "InMempool"
case profileEntryResponse = "ProfileEntryResponse"
case likeCount = "LikeCount"
case diamondCount = "DiamondCount"
case isPinned = "IsPinned"
case commentCount = "CommentCount"
case recloutCount = "RecloutCount"
case diamondsFromSender = "DiamondsFromSender"
}
}
// MARK: - ProfileEntryResponse
struct ProfileEntryResponse: Decodable {
var publicKeyBase58Check, username, profileEntryResponseDescription, profilePic: String?
var isHidden, isReserved, isVerified: Bool?
var coinPriceBitCloutNanos, stakeMultipleBasisPoints: Int?
enum CodingKeys: String, CodingKey {
case publicKeyBase58Check = "PublicKeyBase58Check"
case username = "Username"
case profileEntryResponseDescription = "Description"
case profilePic = "ProfilePic"
case isHidden = "IsHidden"
case isReserved = "IsReserved"
case isVerified = "IsVerified"
case coinPriceBitCloutNanos = "CoinPriceBitCloutNanos"
case stakeMultipleBasisPoints = "StakeMultipleBasisPoints"
}
}
First of all, do not name classes in lowercase like fetchResults, also don't call a class like an action: every programmer expects fetchResults() to be a function call. Use something like ResultFetcher to name this class. And class instance usually will be lowercased class name, or part of it:
#StateObject var resultFetcher = ResultFetcher()
// or
#StateObject var fetcher = ResultFetcher()
You can only use #State inside SwiftUI views, you cannot use it inside ObservableObject. Use #Published as you already do with other variables if you need to update the view after updating that variable, or use an unannotated variable.
In your case, it doesn't look like you need to store this hash string; you can just pass it from the view.
You can use .onChange(of: text) to track changes to the #State or #Binding variables:
TextField("Paste Clout Hexcode Here", text: $text)
.font(.title2)
.padding()
.onChange(of: text) { text in
resultFetcher.updateData(postHashHex: text)
}
Not sure if you need that initial call of updateData with hardcoded hex code, anyway your view model can be updated to this:
class ResultFetcher: ObservableObject {
#Published var clout = Cloutington()
#Published var dataHasLoaded = false
private let initialPostHashHex: String = "8004bb672ad3f46118775cd4b2cb5306c63f6d68787457990bf0d2fda3f7993a"
init() {
updateData(postHashHex: initialPostHashHex)
}
func updateData(postHashHex: String) {
getData(postHashHex: postHashHex) { clout in
self.clout = clout
}
}
private func getData(postHashHex: String, completion: #escaping (Cloutington) -> ()) {
// your code
}
}
Related
I was trying to make a weather api call, the api call needs to have a location. The location that I pass is a variable, but now I want to change the location value based on a TextField's input.
I made the apiKey shorter just for safety measures. There's more code, but it's not relevant.
I just need to know how to change the city variable that is on the WeatherClass using the TextField that is in the cityTextField View.
Thanks.
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
var city: String = ""
func fetch() {
let location = city
let apiKey = "AP8LUYMSTHZ"
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(location)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}//----------------------------------- End of fetch()
}
struct WeatherData: Decodable {
let resolvedAddress: String
let days: [WeatherDays]
}
struct WeatherDays: Hashable, Decodable {
let datetime: String
let tempmax: Double
let tempmin: Double
let description: String
}
struct cityTextField: View {
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city).frame(height:30).multilineTextAlignment(.center).background().cornerRadius(25).padding(.horizontal)
}
}
I already watched a lot of tutorials for similar things buts none of them really helped me.
Try this approach using minor modifications to
func fetch(_ city: String){...} to fetch the weather for the city in your
TextField using .onSubmit{...}
struct ContentView: View {
#StateObject var weatherModel = WeatherClass()
var body: some View {
VStack {
cityTextField(weatherModel: weatherModel)
}
}
}
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city)
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch(city) // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
func fetch(_ city: String) { // <-- here
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}
Alternatively, as suggested by synapticloop, you could use this approach:
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
var body: some View {
TextField("Search city", text: $weatherModel.city) // <-- here
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch() // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
#Published var city: String = "" // <-- here
func fetch() {
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}
I am trying to display a list in a view after a condition. If data is recieved from API than screen will load new view having list showing multiple fields. Here I am showing only one field in the list. The navigation code is working fine and data is also decoded however blank screen appears when clicking list button screen moves to next view but blank screen.
Here is the view in which I am showing list :
import SwiftUI
struct MyPriceList: View {
#StateObject var road = ListAPI()
List
{
ForEach(road.priceRoad)
{
road in
HStack{
Text(road.packageName)
.font(.system(size: 15))
.foregroundColor(.black)
}
}
}
}
struct MyPriceList_Previews: PreviewProvider {
static var previews: some View {
MyPriceList()
}
}
}
The following is the viewmodel in which I have decoded the JSON data and applied the navigation
import Foundation
class ListAPI : ObservableObject
{
#Published var priceRoad = [ResponseList]()
func getList()
{
// url building code //
let list = URLSession.shared.dataTask(with: urlRequest)
{
(data, response, error) in
if let error = error {
print("Error \(error)")
}
if let data = data
{
do
{
let jsonList = try JSONDecoder().decode(PriceList.self, from: data)
let panama = jsonList.response
for pan in panama
{
print(pan.packageName) //successfully printing
}
if jsonList.success==true
{
DispatchQueue.main.async
{
self.navigate = true
self.priceRoad = jsonList.response
}
}
else
{
DispatchQueue.main.async
{
self.errorMessage = jsonList.message
}
}
}
catch
{
print("error \(error)")
}
}
}
list.resume()
} }
This is the data model for Json
struct PriceList : Codable
{
let success: Bool
let message: String
let response: [ResponseList]
enum CodingKeys:String, CodingKey{
case response = "ResponseData"
case success = "IsSuccess"
case message = "Message"
}
}
struct ResponseList:Codable
{
let packageId: Int
let packageName: String
let price: Double
let discountedPrice: Double
let testType: String
let testPackageGroupId: Int?
let SampleType: [SampleTypeList]?
enum CodingKeys:String, CodingKey{
case packageId = "PackageId"
case packageName = "PackageName"
case price = "Price"
case discountedPrice = "DiscountedPrice"
case testType = "Type"
case testPackageGroupId = "TestPackageGroupId"
case SampleType = "SampleTypeList"
}}
struct SampleTypeList:Codable
{
let testSampleTypeId: String
let sampleName: String
let colourCode: String
enum CodingKeys:String, CodingKey{
case testSampleTypeId = "TestSampleTypeId"
case sampleName = "SampleName"
case colourCode = "ColourCode"
}
}
I need to display TestName as packageName, MRP as price, B2B as discountedPrice, and TestType as testType.
try this approach, calling road.getList() in .onAppear{}:
struct MyPriceList: View {
#StateObject var road = ListAPI()
var body: some View { // <-- here need a body
List
{
ForEach(road.priceRoad)
{
road in
HStack{
Text(road.packageName)
.font(.system(size: 15))
.foregroundColor(.black)
}
}
}
.onAppear {
road.getList() // <-- here load your data
}
}
}
and make PriceList and ResponseList Identifiable, like this:
struct PriceList : Identifiable, Codable {
let id = UUID()
// ...
}
struct ResponseList: Identifiable, Codable {
let id = UUID()
// ...
}
struct SampleTypeList: Identifiable, Codable {
let id = UUID()
// ...
}
Alternatively, in ListAPI, you could have init() { getList() }, instead of using .onAppear {road.getList()}
I am creating mobile GitHub repository search app and I've just figured out how to handle no responses with my friend, but this solution doesn't allow to change screens between the the noResults and List views (I commented them for you in var body).
The code:
import SwiftUI
import Combine
private final class ContentViewState: ObservableObject {
#Published var isLoading = false
#Published var query = ""
#Published var stuff = [String]()
#Published var noResults = false
private var subscription: AnyCancellable?
func fetchRepos(query: String) {
isLoading = true
subscription = Just("test")
.delay(for: 2, scheduler: RunLoop.main)
.sink(receiveValue: {[weak self] (title: String) in
self?.isLoading = false
self?.stuff.append(title)
})
}
}
struct ContentView: View {
#StateObject private var state = ContentViewState()
#State private var items = [Item]()
var body: some View {
VStack {
if state.isLoading {
ProgressView()
}
//noResults view here
else if state.noResults {
HStack {
TextField("Enter search", text: $state.query)
Button("Search") {
state.fetchRepos(query: state.query)
}
}
Text("No results... Try again!")
}
//List view here
else {
HStack {
TextField("Enter search", text: $state.query)
Button("Search") {
state.fetchRepos(query: state.query)
}
}
List(items, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.fullName).font(.headline)
Text(item.urlCode)
}
}.task {
await loadData()
}
}
}
}
func loadData() async {
guard let url = URL(string: "https://api.github.com/search/repositories?q=" + state.query + "&per_page=20") else
{
DispatchQueue.main.async {
state.noResults = true }
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Root.self, from: data) {
items = decodedResponse.items
if items.isEmpty {
DispatchQueue.main.async {
state.noResults = true }
}
}
} catch {
DispatchQueue.main.async {
state.noResults = true}
}
}
}
The problem is, if you pass the valid input, i.e.: Data, Core, then you get the search results loaded into the app.
If you pass the invalid input, i.e.: 'Brigmhnst', you get noResults view, but if you pass the valid input from that state, you won't get the List view.
Both views look almost the same. The only difference is one of them is else if and the other is else and I can't have two 'elses', can I?
I have already tried adding the start-point view (similar to list View) bound to else condition, another list View with else if and created an else condition inside do in func loadData that would run if the items were not empty. The app seemed to work, but the first start-point view wouldn't swap to listView after passing the data in it (it is not in the code, since this solution worked worse than that I already have, but I can add it if you want to see it).
There are a few problems with the code you have.
First, I believe the models (which are not included) are incorrect. I pasted the JSON into quicktype.io and this is the result:
import Foundation
// MARK: - Repos
struct Repos: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [Item]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
// MARK: - Item
struct Item: Codable {
let id: Int
let nodeID, name, fullName: String
let itemPrivate: Bool
let owner: Owner
let htmlURL: String
let itemDescription: String?
let fork: Bool
let url, forksURL: String
let keysURL, collaboratorsURL: String
let teamsURL, hooksURL: String
let issueEventsURL: String
let eventsURL: String
let assigneesURL, branchesURL: String
let tagsURL: String
let blobsURL, gitTagsURL, gitRefsURL, treesURL: String
let statusesURL: String
let languagesURL, stargazersURL, contributorsURL, subscribersURL: String
let subscriptionURL: String
let commitsURL, gitCommitsURL, commentsURL, issueCommentURL: String
let contentsURL, compareURL: String
let mergesURL: String
let archiveURL: String
let downloadsURL: String
let issuesURL, pullsURL, milestonesURL, notificationsURL: String
let labelsURL, releasesURL: String
let deploymentsURL: String
let createdAt, updatedAt, pushedAt: String // Quicktype said these were dates, that didn't work
let gitURL, sshURL: String
let cloneURL: String
let svnURL: String
let homepage: String?
let size, stargazersCount, watchersCount: Int
let language: JSONNull?
let hasIssues, hasProjects, hasDownloads, hasWiki: Bool
let hasPages: Bool
let forksCount: Int
let mirrorURL: JSONNull?
let archived, disabled: Bool
let openIssuesCount: Int
let license: JSONNull?
let allowForking, isTemplate: Bool
let topics: [String]
let visibility: String
let forks, openIssues, watchers: Int
let defaultBranch: String
let score: Int
enum CodingKeys: String, CodingKey {
case id
case nodeID = "node_id"
case name
case fullName = "full_name"
case itemPrivate = "private"
case owner
case htmlURL = "html_url"
case itemDescription = "description"
case fork, url
case forksURL = "forks_url"
case keysURL = "keys_url"
case collaboratorsURL = "collaborators_url"
case teamsURL = "teams_url"
case hooksURL = "hooks_url"
case issueEventsURL = "issue_events_url"
case eventsURL = "events_url"
case assigneesURL = "assignees_url"
case branchesURL = "branches_url"
case tagsURL = "tags_url"
case blobsURL = "blobs_url"
case gitTagsURL = "git_tags_url"
case gitRefsURL = "git_refs_url"
case treesURL = "trees_url"
case statusesURL = "statuses_url"
case languagesURL = "languages_url"
case stargazersURL = "stargazers_url"
case contributorsURL = "contributors_url"
case subscribersURL = "subscribers_url"
case subscriptionURL = "subscription_url"
case commitsURL = "commits_url"
case gitCommitsURL = "git_commits_url"
case commentsURL = "comments_url"
case issueCommentURL = "issue_comment_url"
case contentsURL = "contents_url"
case compareURL = "compare_url"
case mergesURL = "merges_url"
case archiveURL = "archive_url"
case downloadsURL = "downloads_url"
case issuesURL = "issues_url"
case pullsURL = "pulls_url"
case milestonesURL = "milestones_url"
case notificationsURL = "notifications_url"
case labelsURL = "labels_url"
case releasesURL = "releases_url"
case deploymentsURL = "deployments_url"
case createdAt = "created_at"
case updatedAt = "updated_at"
case pushedAt = "pushed_at"
case gitURL = "git_url"
case sshURL = "ssh_url"
case cloneURL = "clone_url"
case svnURL = "svn_url"
case homepage, size
case stargazersCount = "stargazers_count"
case watchersCount = "watchers_count"
case language
case hasIssues = "has_issues"
case hasProjects = "has_projects"
case hasDownloads = "has_downloads"
case hasWiki = "has_wiki"
case hasPages = "has_pages"
case forksCount = "forks_count"
case mirrorURL = "mirror_url"
case archived, disabled
case openIssuesCount = "open_issues_count"
case license
case allowForking = "allow_forking"
case isTemplate = "is_template"
case topics, visibility, forks
case openIssues = "open_issues"
case watchers
case defaultBranch = "default_branch"
case score
}
}
// MARK: - Owner
struct Owner: Codable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
let url, htmlURL, followersURL: String
let followingURL, gistsURL, starredURL: String
let subscriptionsURL, organizationsURL, reposURL: String
let eventsURL: String
let receivedEventsURL: String
let type: String
let siteAdmin: Bool
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
case url
case htmlURL = "html_url"
case followersURL = "followers_url"
case followingURL = "following_url"
case gistsURL = "gists_url"
case starredURL = "starred_url"
case subscriptionsURL = "subscriptions_url"
case organizationsURL = "organizations_url"
case reposURL = "repos_url"
case eventsURL = "events_url"
case receivedEventsURL = "received_events_url"
case type
case siteAdmin = "site_admin"
}
}
// 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 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()
}
}
Next, your fetchRepos function isn't doing anything. It's really only presenting a loader and stopping that later. The loadData function is actually what you want to use.
Finally, if you keep the task of loadData near the end of the code, you'll be in an infinite loop.
So these changes will clean it up a bit.
#MainActor private final class ContentViewState: ObservableObject {
#Published var isLoading = false
#Published var query = ""
#Published var items = [Item]()
func loadData(query: String = "") async {
guard let url = URL(string: "https://api.github.com/search/repositories?q=" + query + "&per_page=20") else {
return
}
isLoading = true
defer { isLoading = false }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decodedResponse = try JSONDecoder().decode(Repos.self, from: data)
items = decodedResponse.items
} catch {
print("error \(error)")
}
}
}
struct ContentView: View {
#StateObject private var state = ContentViewState()
var searchView: some View {
HStack {
TextField("Enter search", text: $state.query)
Button("Search") {
Task {
await state.loadData(query: state.query)
}
}
}
}
var body: some View {
if state.isLoading {
ProgressView()
} else {
VStack {
searchView
if state.items.isEmpty {
Text("No results...\nTry again!")
} else {
List(state.items) { item in
VStack(alignment: .leading) {
Text(item.fullName).font(.headline)
Text(item.htmlURL)
}.padding(.vertical)
}
}
}.padding()
}
}
}
The last thing you'll need to do is conform Item to Identifiable
extension Item: Identifiable {} // You could also just add Identifiable to the actual declaration
This question builds on my previous question. Basically Im making an async call to the Google Books Api when a certain button is pressed. While I got the call working when its a method of the View however I want to overlay an activity indicator while it's loading. Hence I tried making an ObservableObject to make the call instead but Im not sure how to do it.
Here's what I have so far:
class GoogleBooksApi: ObservableObject {
enum LoadingState<Value> {
case loading(Double)
case loaded(Value)
}
#Published var state: LoadingState<GoogleBook> = .loading(0.0)
enum URLError : Error {
case badURL
}
func fetchBook(id identifier: String) async throws {
var components = URLComponents(string: "https://www.googleapis.com/books/v1/volumes")
components?.queryItems = [URLQueryItem(name: "q", value: "isbn=\(identifier)")]
guard let url = components?.url else { throw URLError.badURL }
self.state = .loading(0.25)
let (data, _) = try await URLSession.shared.data(from: url)
self.state = .loading(0.75)
self.state = .loaded(try JSONDecoder().decode(GoogleBook.self, from: data))
}
}
struct ContentView: View {
#State var name: String = ""
#State var author: String = ""
#State var total: String = ""
#State var code = "ISBN"
#ObservedObject var api: GoogleBooksApi
var body: some View {
VStack {
Text("Name: \(name)")
Text("Author: \(author)")
Text("total: \(total)")
Button(action: {
code = "978-0441013593"
Task {
do {
try await api.fetchBook(id: code)
let fetchedBooks = api.state
let book = fetchedBooks.items[0].volumeInfo
name = book.title
author = book.authors?[0] ?? ""
total = String(book.pageCount!)
} catch {
print(error)
}
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
// MARK: - GoogleBook
struct GoogleBook: Codable {
let kind: String
let totalItems: Int
let items: [Item]
}
// MARK: - Item
struct Item: Codable {
let id, etag: String
let selfLink: String
let volumeInfo: VolumeInfo
}
// MARK: - VolumeInfo
struct VolumeInfo: Codable {
let title: String
let authors: [String]?
let pageCount: Int?
let categories: [String]?
enum CodingKeys: String, CodingKey {
case title, authors
case pageCount, categories
}
}
and this is what works without the loading states:
struct ContentView: View {
#State var name: String = ""
#State var author: String = ""
#State var total: String = ""
#State var code = "ISBN"
enum URLError : Error {
case badURL
}
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)
}
var body: some View {
VStack {
Text("Name: \(name)")
Text("Author: \(author)")
Text("total: \(total)")
Button(action: {
code = "978-0441013593"
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)
}
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
// MARK: - GoogleBook
struct GoogleBook: Codable {
let kind: String
let totalItems: Int
let items: [Item]
}
// MARK: - Item
struct Item: Codable {
let id, etag: String
let selfLink: String
let volumeInfo: VolumeInfo
}
// MARK: - VolumeInfo
struct VolumeInfo: Codable {
let title: String
let authors: [String]?
let pageCount: Int?
let categories: [String]?
enum CodingKeys: String, CodingKey {
case title, authors
case pageCount, categories
}
}
I would go a step further and add idle and failed states.
Then instead of throwing an error change the state to failed and pass the error description. I removed the Double value from the loading state to just show a spinning ProgressView
#MainActor
class GoogleBooksApi: ObservableObject {
enum LoadingState {
case idle
case loading
case loaded(GoogleBook)
case failed(Error)
}
#Published var state: LoadingState = .idle
func fetchBook(id identifier: String) async {
var components = URLComponents(string: "https://www.googleapis.com/books/v1/volumes")
components?.queryItems = [URLQueryItem(name: "q", value: "isbn=\(identifier)")]
guard let url = components?.url else { state = .failed(URLError(.badURL)); return }
self.state = .loading
do {
let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(GoogleBook.self, from: data)
self.state = .loaded(response)
} catch {
state = .failed(error)
}
}
}
In the view you have to switch on the state and show different views.
And – very important – you have to declare the observable object as #StateObject. This is a very simple implementation
struct ContentView: View {
#State var code = "ISBN"
#StateObject var api = GoogleBooksApi()
var body: some View {
VStack {
switch api.state {
case .idle: EmptyView()
case .loading: ProgressView()
case .loaded(let books):
if let info = books.items.first?.volumeInfo {
Text("Name: \(info.title)")
Text("Author: \(info.authors?.joined(separator: ", ") ?? "")")
Text("total: \(books.totalItems)")
}
case .failed(let error):
if error is DecodingError {
Text(error.description)
} else {
Text(error.localizedDescription)
}
}
Button(action: {
code = "978-0441013593"
Task {
await api.fetchBook(id: code)
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
It seems like you're not initializing the GoogleBooksApi.
#ObservedObject var api: GoogleBooksApi
neither any init where it can be modified.
Other than that - I'd suggest using #StateObject (provided you deployment target is minimum iOS 14.0). Using ObservableObject might lead to multiple initializations of the GoogleBooksApi (whereas you need only once)
You should use #StateObject for any observable properties that you
initialize in the view that uses it. If the ObservableObject instance
is created externally and passed to the view that uses it mark your
property with #ObservedObject.
I've been having some trouble with my swift package called UnsplashSwiftUI
Before WWDC, I was having some trouble which caused my View to reload (as you can see on the main branch) but when async/await was announced, it seemed to be the perfect opportunity for my package.
I am working on the package with async/await on the development branch.
However, I am now having some trouble with the async API request.
Here's my minimally reproducible example, I get the printed error 'Failed to fetch image' from the catch block of my async function getURL(). I also tried calling the task with async inside.
//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()
}
}
In your models, UnsplashData and User, replace Date? with String?.
After that, this is how I tested my answer:
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
}
}
}