async request to Unsplash api not working correctly - swift

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
}
}
}

Related

No results view freezes; doesn't change after adding new input

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

Displaying State of an Async Api call in SwiftUI

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.

Passing text between view and class SwiftUI

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
}
}

Swiift - Decoding json data from server

Im working on a project where I have to display data from a network call. The problem is Im having trouble decoding data the data that I received from the network call and storing it into structs variable to use for other calls. The deadline is coming close and Im not sure why my code does not work. This is json that I receive back
{"result":{"login":{"isAuthorized":true,"isEmpty":false,"userName":{"isEmpty":false,"name":{"firstName":"Jason","lastName":"Test","displayName":"Test, Jason","isEmpty":false,"fullName":"Jason Test"},"canDelete":false,"id":5793,"canModify":false},"username":"test#testable.com"},"parameters":{"isEmpty":false,"keep_logged_in_indicator":false,"username":"test#testable.com"}},"isAuthorized":true,"version":{"major":"2021","minor":"004","fix":"04","display":"2021.004.04","isEmpty":false},"isSystemDown":false,"timestamp":"2021-07-26T20:21:43Z","isSuccess":true}
This is the different struct that I made in my project
struct ApiResponse: Decodable {
let results: Results?
let isAuthorized: Bool?
let version: Version?
let isSystemDown: Bool?
let errors: [serverError]?
let timestamp: Date?
let isSuccess: Bool?
}
// MARK: - Result
struct Results: Decodable {
let login: Login
let parameters: Parameters?
}
// MARK: - Login
struct Login: Decodable {
let isAuthorized, isEmpty: Bool?
let userName: UserName
let username: String?
}
// MARK: - UserName
struct UserName: Decodable {
let isEmpty: Bool?
let name: Name
let canDelete: Bool?
let id: Int
let canModify: Bool?
}
// MARK: - Name
struct Name: Decodable {
let firstName, lastName, displayName: String
let isEmpty: Bool?
let fullName: String
}
// MARK: - Parameters
struct Parameters: Decodable {
let isEmpty, keepLoggedInIndicator: Bool?
let username: String?
enum CodingKeys: String, CodingKey {
case isEmpty
case keepLoggedInIndicator
case username
}
}
// MARK: - Version
struct Version: Decodable {
let major, minor, fix, display: String?
let isEmpty: Bool?
}
// Mark: - Error
struct serverError: Decodable {
let password: String?
}
The code that I am using to decode the json data is this
private func handleResponse<T: Decodable>(result: Result<Data, Error>?, completion: (Result<T, Error>) -> Void) {
guard let result = result else {
completion(.failure(AppError.unknownError))
return
}
switch result {
case .success(let data):
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
print("Server JsonObject response: \(json)")
} catch {
completion(.failure(error))
}
let decoder = JSONDecoder()
// decodes the Server response
guard let response = try? decoder.decode(ApiResponse.self, from: data) else {
print("Something happen here")
completion(.failure(AppError.errorDecoding))
return
}
// Returns if an error occurs
if let error = response.errors {
completion(.failure(AppError.serverError(error)))
return
}
// Decodes the data received from server
if let decodedData = response.results {
completion(.success(decodedData as! T))
} else {
completion(.failure(AppError.errorDecoding))
}
case .failure(let error):
completion(.failure(error))
}
}
If anyone could help me understand why my code isnt working that would be much appreciated.
Your struct is wrong. Try using this?
struct YourAPIData {
struct Root: Codable {
struct Result: Codable {
struct Login: Codable {
let isAuthorized: Bool
let isEmpty: Bool
struct UserName: Codable {
let isEmpty: Bool
struct Name: Codable {
let firstName: String
let lastName: String
let displayName: String
let isEmpty: Bool
let fullName: String
}
let name: Name
let canDelete: Bool
let id: Int
let canModify: Bool
}
let userName: UserName
let username: String
}
let login: Login
struct Parameters: Codable {
let isEmpty: Bool
let keepLoggedInIndicator: Bool
let username: String
private enum CodingKeys: String, CodingKey {
case isEmpty
case keepLoggedInIndicator = "keep_logged_in_indicator"
case username
}
}
let parameters: Parameters
}
let result: Result
let isAuthorized: Bool
struct Version: Codable {
let major: String
let minor: String
let fix: String
let display: Date
let isEmpty: Bool
}
let version: Version
let isSystemDown: Bool
let timestamp: String
let isSuccess: Bool
}
}
and try using
do {
let APIData = try JSONDecoder().decode(YourAPIData.Root.self, from: jsonData)
} catch let jsonErr { print("Error: ", jsonErr) }
And if you would like to display your json (for testing, i suppose?)
if let data = jsonData, let body = String(data: jsonData, encoding: .utf8) {
print(body)
}
} else {
print(error ?? "Unknown error")
}

Cannot map protocol compliant elements to generic elements

As you can see below, I downloaded an array of structures containing heterogeneous objects that were decoded into enums containing nested objects.
I would now like to put said objects into a generic Model structure, but the compiler won't allow this - the error is described below in the code comment. I am relatively new to programming in Swift, I would appreciate your help.
import Foundation
let jsonString = """
{
"data":[
{
"type":"league",
"info":{
"name":"NBA",
"sport":"Basketball",
"website":"https://nba.com/"
}
},
{
"type":"player",
"info":{
"name":"Kawhi Leonard",
"position":"Small Forward",
"picture":"https://i.ibb.co/b5sGk6L/40a233a203be2a30e6d50501a73d3a0a8ccc131fv2-128.jpg"
}
},
{
"type":"team",
"info":{
"name":"Los Angeles Clippers",
"state":"California",
"logo":"https://logos-download.com/wp-content/uploads/2016/04/LA_Clippers_logo_logotype_emblem.png"
}
}
]
}
"""
struct Response: Decodable {
let data: [Datum]
}
struct League: Codable {
let name: String
let sport: String
let website: URL
}
extension League: Displayable {
var text: String { name }
var image: URL { website }
}
struct Player: Codable {
let name: String
let position: String
let picture: URL
}
extension Player: Displayable {
var text: String { name }
var image: URL { picture }
}
struct Team: Codable {
let name: String
let state: String
let logo: URL
}
extension Team: Displayable {
var text: String { name }
var image: URL { logo }
}
enum Datum: Decodable {
case league(League)
case player(Player)
case team(Team)
enum DatumType: String, Decodable {
case league
case player
case team
}
private enum CodingKeys : String, CodingKey { case type, info }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(DatumType.self, forKey: .type)
switch type {
case .league:
let item = try container.decode(League.self, forKey: .info)
self = .league(item)
case .player:
let item = try container.decode(Player.self, forKey: .info)
self = .player(item)
case .team:
let item = try container.decode(Team.self, forKey: .info)
self = .team(item)
}
}
}
protocol Displayable {
var text: String { get }
var image: URL { get }
}
struct Model<T: Displayable> {
let text: String
let image: URL
init(item: T) {
self.text = item.text
self.image = item.image
}
}
do {
let response = try JSONDecoder().decode(Response.self, from: Data(jsonString.utf8))
let items = response.data
let models = items.map { (item) -> Model<Displayable> in // error: only struct/enum/class types can conform to protocols
switch item {
case .league(let league):
return Model(item: league)
case .player(let player):
return Model(item: player)
case .team(let team):
return Model(item: team)
}
}
} catch {
print(error)
}
You do not need generics here.
Change Model to accept any type that conforms to Displayable in the init
struct Model {
let text: String
let image: URL
init(item: Displayable) {
self.text = item.text
self.image = item.image
}
}
and then change the closure to return Model
let models = items.map { (item) -> Model in
If you want to keep your Model struct generic then you need to change the map call to
let models: [Any] = items.map { item -> Any in
switch item {
case .league(let league):
return Model(item: league)
case .player(let player):
return Model(item: player)
case .team(let team):
return Model(item: team)
}
}
This will give the following output when conforming to CustomStringConvertible
extension Model: CustomStringConvertible {
var description: String {
"\(text) type:\(type(of: self))"
}
}
print(models)
[NBA type:Model<League>, Kawhi Leonard type:Model<Player>, Los Angeles Clippers type:Model<Team>]
If you are only interested in name and the key representing the URL parse the JSON directly into Model by using nestedContainer this way
struct Response: Decodable {
let data: [Model]
}
struct Model: Decodable {
let name: String
let image: URL
enum DatumType: String, Decodable {
case league
case player
case team
}
private enum CodingKeys : String, CodingKey { case type, info }
private enum CodingSubKeys : String, CodingKey { case name, website, picture, logo }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(DatumType.self, forKey: .type)
let subContainer = try container.nestedContainer(keyedBy: CodingSubKeys.self, forKey: .info)
self.name = try subContainer.decode(String.self, forKey: .name)
let urlKey : CodingSubKeys
switch type {
case .league: urlKey = .website
case .player: urlKey = .picture
case .team: urlKey = .logo
}
self.image = try subContainer.decode(URL.self, forKey: urlKey)
}
}
do {
let response = try JSONDecoder().decode(Response.self, from: Data(jsonString.utf8))
let data = response.data
print(data)
} catch {
print(error)
}