Fetching and decoding unkwown error in Swift [closed] - swift

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
I'm trying to fetch the data and decode it into structs, and then display that data in a list.
I used quicktype.io to parse JSON to Swift. This part seems to be okay.
struct Response: Codable {
var results: [User]
}
struct User: Codable {
let id: String
let isActive: Bool
let name: String
let age: Int
let company, email, address, about: String
let registered: Date
let tags: [String]
let friends: [Friend]
}
struct Friend: Codable {
let id, name: String
}
This is the rest of the code, where I try to decode the data(where the error occurs). Here is a link to the JSON.
struct ContentView: View {
#State private var users = [User]()
var body: some View {
List {
ForEach(users, id: \.id) { user in
Text(user.name)
}
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
print(decodedResponse.results)
users = decodedResponse.results
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}

You have to decode on type [User] not Response. Look at the response and you'll see that the top-level JSON is an Array, not a Dictionary. Also, update your code to get meaningful error messages. Use do try catch syntax instead of if let try?.
func loadData() {
guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let users = try decoder.decode([User].self, from: data)
DispatchQueue.main.async {
print(users)
self.users = users
}
} catch {
print(error)
}
} else {
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}
}.resume()
}
Update: Also, need to add dateDecodingStrategy to make the decoding work correctly.

Related

Why is URLSession not returning data in playground?

I cannot understand why this URLSession is not working on my playground. The URL works fine using curl commands on my terminal, so I know it is active, but I cannot see any list of names printed on my console.
The only print I see is the one "called" after that one, it seems there must be some error, but I have no clue about it, no message. Issue must be around the URLSession but cannot get where.
UPDATE
I added an extension to data found on stack, using right after this code
let (data, response) = try await URLSession.shared.data(from: url)
data.printJson()
The data is printed, but still cannot print anything in the for loop, where it should be.
extension Data {
func printJson() {
do {
let json = try JSONSerialization.jsonObject(with: self, options: [])
let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
guard let jsonString = String(data: data, encoding: .utf8) else {
print("Invalid data")
return
}
print(jsonString)
} catch {
print("Error: \(error.localizedDescription)")
}
}
}
And
import Foundation
//-----------------------------------------------
//MARK: - model
//-----------------------------------------------
struct ResponseRandom: Codable {
let users: [User]
}
struct User: Codable, Identifiable {
let id: Int
let first_name: String
let email: String
// "avatar": "https://robohash.org/rationeetsit.png?size=300x300&set=set1"
let date_of_birth: String//"1958-07-18"
}
//-----------------------------------------------
//MARK: - class
//-----------------------------------------------
class HTTPRequest_randomUsers {
// #Published var users = [UserRandom]()
init() {
Task {
await loadData()
}
}
func loadData() async {
print("called")
let numberOfItems = 50
guard let url = URL(string: "https://random-data-api.com/api/v2/users?size=2&response_type=json") else {
fatalError("URL error")
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse else {
print("not valid response")
return}
guard response.statusCode == 200 else {
print("not 200 status")
return}
let decoded = try JSONDecoder().decode([User].self, from: data)
print("decoded")
await MainActor.run {
// users = decoded.users
for item in decoded {
print(item.first_name)
}
}
} catch {
print("error: \(error)")
}
}
}
//here my call
let c = HTTPRequest_randomUsers()
Network requests run asynchronously (i.e., finish later). But by default, simple Playgrounds will stop when they reach the end of their path of execution. One must tell the Playground to continue execution indefinitely in order for the asynchronous results to be retrieved successfully:
import PlaygroundSupport
and
PlaygroundPage.current.needsIndefiniteExecution = true

How to bind data to ViewModel for showing it on UI in MVVM?

In my app I am using MVVM pattern.
Below is my Model.
struct NewsModel: Codable {
let status: String
let totalResults: Int
let articles: [Article]
}
struct Article: Codable {
let source: Source
let author: String?
let title: String
let articleDescription: String?
let url: String
let urlToImage: String?
let publishedAt: Date
let content: String?
enum CodingKeys: String, CodingKey {
case source, author, title
case articleDescription = "description"
case url, urlToImage, publishedAt, content
}
}
struct Source: Codable {
let id: String?
let name: String
}
Below is my ViewModel. Which is used for show the data from API.
struct NewsArticleViewModel {
let article: Article
var title:String {
return self.article.title
}
var publication:String {
return self.article.articleDescription!
}
var imageURL:String {
return self.article.urlToImage!
}
}
Below is my API request class.
class Webservice {
func getTopNews(completion: #escaping (([NewsModel]?) -> Void)) {
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb") else {
fatalError("URL is not correct!!!")
}
URLSession.shared.dataTask(with: url) {
data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
let news = try? JSONDecoder().decode([NewsModel].self, from: data)
DispatchQueue.main.async {
completion(news)
}
}.resume()
}
}
After receiving response from my API I want to show it on screen. For this I added below ViewModel.
class NewsListViewModel: ObservableObject {
#Published var news: [NewsArticleViewModel] = [NewsArticleViewModel]()
func load() {
fetchNews()
}
private func fetchNews() {
Webservice().getTopNews {
news in
if let news = news {
//How to bind this data to NewsArticleViewModel and show it on UI?
}
}
}
}
Please let me know. What I have to write there for showing it on UI.
According to the documentation of newsapi.org your request will return one NewsModel object not an array. So change your Webservice class to:
class Webservice {
//Change the completion handler to return an array of Article
func getTopNews(completion: #escaping (([Article]?) -> Void)) {
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb") else {
fatalError("URL is not correct!!!")
}
URLSession.shared.dataTask(with: url) {
data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
// decode to a single NewsModel object instead of an array
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let news = try? decoder.decode(NewsModel.self, from: data)
DispatchQueue.main.async {
// completion with an optional array of Article
completion(news?.articles)
}
}.resume()
}
}
You would need to map those received values to NewsArticleViewModel types. For example:
Webservice().getTopNews { articles in
if let articles = articles {
self.news = articles.map{NewsArticleViewModel(article: $0)}
}
}
And remove let news: NewsModel from the NewsArticleViewModel struct as it is not needed.
Edit:
It seems:
let publishedAt: Date
is throwing an error. Jsondecoder fails to interpret the string to a date. Change your Webservice. I´ve updated it in my answer.
You could remove the legacy MVVM pattern and do it in proper SwiftUI like this:
struct ContentView: View {
#State private var articles = [Article]()
var body: some View {
NavigationView {
List(articles) { article in
Text(article.title)
}
.navigationTitle("Articles")
}
.task {
do {
let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb")!
let (data, _) = try await URLSession.shared.data(from: url)
articles = try JSONDecoder().decode([Article].self, from: data)
} catch {
articles = []
}
}
}
}

How could you access the GitHub API in Swift?

I'd like to make an update detection system in my macOS SwiftUI app by pulling the latest release from GitHub via the API and then comparing the tag. How would I go about accessing the API from Swift? I've tried using the methods from here, medium.com, here, swifttom.com and here, steveclarkapps.com but none of them accomplish what I'm trying to do.
For the first method, the code functions with the provided example API, but doesn't work with the GitHub API and it returns this error instead:
Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))
Method 2 suffers the same issue.
I couldn't even get enough of method 3's code working to try it.
Here's my adapted code based off of the medium.com method:
Model.swift
import Foundation
struct TaskEntry: Codable {
let id: Int
let tag_name: String
let name: String
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.name)
}
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
/*if*/ let response = try! JSONDecoder().decode([TaskEntry].self, from: data) /*{*/
DispatchQueue.main.async {
self.results = response
}
return
/*}*/
}
}.resume()
}
}
Commented out code and variable names that seem irrelevant are just leftovers.
OS: macOS Big Sur 11.6
Xcode version: 13.0
Open this in your browser:
https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest
You will notice it is not an array but an object. You should be decoding an object like this:
JSONDecoder().decode(TaskEntry.self, from: data)
Edit:
This requires you to change your view. Notice this is no longer a List because you are no longer fetching an array but a single item:
struct TaskEntry: Codable {
let id: Int
let tagName: String
let name: String
}
struct ContentView: View {
#State var entry: TaskEntry? = nil
var body: some View {
VStack(alignment: .leading) {
if let entry = entry {
Text("\(entry.id)")
Text(entry.name)
Text(entry.tagName)
} else {
ProgressView()
}
}
.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
// TODO: Handle data task error
return
}
guard let data = data else {
// TODO: Handle this
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let response = try decoder.decode(TaskEntry.self, from: data)
DispatchQueue.main.async {
self.entry = response
}
} catch {
// TODO: Handle decoding error
print(error)
}
}.resume()
}
}
NOTICE: I did some other improvements as well
Use JSONDecoder to convert from snake case to camel case
Added do catch block so your app doesn't crash
Check for errors before decoding
Added loading indicator (had to put something in the else)
However,
As our discussion you are probably calling the wrong endpoint. That endpoint is not returning an array but a single object, you can tell this because the JSON response begins with { rather than [
I've adjusted my answer to change the endpoint I believe you should be calling:
struct TaskEntry: Codable {
let id: Int
let tagName: String
let name: String
}
struct ContentView: View {
#State var results: [TaskEntry]? = nil
var body: some View {
if let results = results {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.name)
}
}
} else {
VStack(alignment: .leading) {
ProgressView()
.onAppear(perform: loadData)
}
}
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
// TODO: Handle data task error
return
}
guard let data = data else {
// TODO: Handle this
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let response = try decoder.decode([TaskEntry].self, from: data)
DispatchQueue.main.async {
self.results = response
}
} catch {
// TODO: Handle decoding error
print(error)
}
}.resume()
}
}

Data is missing from API call to Unsplashed using Swift [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
Problem:
I was following a tutorial and I cannot seem to get the data loaded when I get a API call from Unsplashed. I registered for an account and used my personal key.
I created my loadData function that should load the data from the API call:
func loadData() {
let key = "my key"
let url = "https://api.unsplash.com/photos/random/?count=30&client_id=\(key)"
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url)!) { (data, _, error) in
guard let data = data else {
print("URLSession dataTask error", error ?? "nil")
return
}
print(data)
do{
let json = try JSONDecoder().decode([Photo].self,from: data)
print(json)
for photo in json {
DispatchQueue.main.async {
self.photoArray.append(photo)
}
}
}catch{
print("In error")
print(error.localizedDescription)
}
}.resume()
}
I present my data in the content view as follows:
import SwiftUI
struct ContentView: View {
#ObservedObject var randomImages = UnsplashData()
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I print the data before I use the JSON decoder and there seems to be data there, but when I go to get the JSON it states this error:
The data couldn’t be read because it is missing.
So is it the way I am using the API key? Or something else?
The photo structure is as follows:
struct Photo: Identifiable, Decodable {
var id: String
var alt_description: String
var urls: [String: String]
}
It's not the data that you're receiving, it's how you're decoding it that seems to be an issue.
For example, this simple playground example works just fine...
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
struct Sources: Decodable {
let raw: String
let full: String
let regular: String
let small: String
let thumbnail: String
}
struct Photo: Decodable {
let urls: [Sources]
}
let key = "don't post this, that's a bad idea"
let url = "https://api.unsplash.com/photos/random/?count=30&client_id=\(key)"
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url)!) { (data, _, error) in
guard let data = data else {
print("URLSession dataTask error", error ?? "nil")
return
}
print(String(data: data, encoding: .utf8))
do{
let json = try JSONDecoder().decode([Photo].self,from: data)
print(json)
}catch{
print("In error")
print(error.localizedDescription)
}
}.resume()
If you change print(error.localizedDescription) to print(error) you'll generally get a more accurate description of the error

Error when parsing JSON data (Swift 4 Playground)

My Swift Playground keeps returning
Error: The data couldn't be read because it isn't in the correct
format."
and I can't figure out what I'm doing wrong. Below is my code.
JSON Sample Data:
{
"meta": {
"name":"Tour of Honor Bonus Listing",
"version":"18.1.4"
},
"bonuses": [
{
"bonusCode":"AZ1",
"category":"ToH",
"name":"Anthem Tour of Honor Memorial",
"value":1,
"city":"Anthem",
"state":"AZ",
"flavor":"Flavor Text Goes Here"
}
]
}
Playground Code:
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
struct JsonFile: Codable {
struct Meta: Codable {
let name: String
let version: String
}
struct JsonBonuses: Codable {
let bonusCode: String
let category: String
let name: String
let value: Int
let city: String
let state: String
let flavor: String
}
let meta: Meta
let bonuses: [JsonBonuses]
}
let url = URL(string: "http://www.tourofhonor.com/BonusData.json")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error: \(error.localizedDescription)")
PlaygroundPage.current.finishExecution()
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
print("Error: invalid HTTP response code")
PlaygroundPage.current.finishExecution()
}
guard let data = data else {
print("Error: missing data")
PlaygroundPage.current.finishExecution()
}
// feel free to uncomment this for debugging data
// print(String(data: data, encoding: .utf8))
do {
let decoder = JSONDecoder()
let posts = try decoder.decode([JsonFile].self, from: data)
print(posts.map { $0.meta.name })
PlaygroundPage.current.finishExecution()
}
catch {
print("Error: \(error.localizedDescription)")
PlaygroundPage.current.finishExecution()
}
}.resume()
I assume I have something in my Struct incorrect, but I can't figure out what it is.
(This paragraph is to make the submission tool happy because it says I have too much code and not enough other details. Apparently being direct and succinct is not compatible with the submission scanning function).
The struct is correct but the root object is not an array (remove the brackets)
let posts = try decoder.decode(JsonFile.self, from: data)
print(posts.bonuses.map{$0.name})