class Network {
func getingData(completion : #escaping ([Model]) -> ()) async {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let posts = try? JSONDecoder().decode([Model].self, from: data) {
completion(posts)
}
}
catch {
print("error")
}
}
}
You could try something like this approach, where getData works with Decodable, as was mentioned in the previous answer.
In this particular example an array of Decodable.
struct Post: Decodable, Identifiable {
let userId: Int
let id: Int
let title: String
let body: String
var comments: [Comment]?
}
struct Comment: Decodable, Identifiable {
let postId: Int
let id: Int
let name: String
let email: String
let body: String
}
struct ContentView: View {
let client = Network()
#State var posts: [Post] = []
var body: some View {
List {
ForEach(posts, id: \.id) { post in
Text(post.title)
}
}
.task {
posts = await client.getData(from: "https://jsonplaceholder.typicode.com/posts")
// all comments from the first post
let comments: [Comment] = await client.getData(from: "https://jsonplaceholder.typicode.com/posts/\(posts[0].id)/comments")
print("\n---> comments: \(comments)")
}
}
}
class Network {
func getData<T: Decodable>(from urlString: String) async -> [T] {
guard let url = URL(string: urlString) else {
print(URLError(.badURL))
return [] // <-- todo, deal with errors
}
do {
let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
print(URLError(.badServerResponse))
return [] // <-- todo, deal with errors
}
let results = try JSONDecoder().decode([T].self, from: data)
return results
}
catch {
return [] // <-- todo, deal with errors
}
}
}
Is it what you're looking for?
import Foundation
class Network {
func getingData<Model: Decodable>(completion : #escaping ([Model]) -> ()) async {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let posts = try? JSONDecoder().decode([Model].self, from: data) {
completion(posts)
}
} catch {
print("error")
}
}
}
If so, you only need to declare the Model type as generic. The only thing you need Model to conform is Decodable (the requirement of the JSONDecoder().decode([Model].self, from: data) call).
Related
I am a beginner in iOS development. I was trying to use an api URl: https://www.arbeitnow.com/api/job-board-api in my job search iOS app. But nothing shows on my app. I tested the URL in POSTMAN and it returns json(but HTML in description part?). I wrote the code:
func getResults(completed: #escaping (Result<[Results], ErrorMessage>) -> Void) {
let urlString = "https://www.arbeitnow.com/api/job-board-api"
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let _ = error {
completed(.failure(.invalidData))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
completed(.failure(.invalidResponse))
return
}
guard let data = data else {
completed(.failure(.invalidData))
return
}
do {
let deconder = JSONDecoder()
deconder.keyDecodingStrategy = .convertFromSnakeCase
let results = try deconder.decode([Results].self, from: data)
completed(.success(results))
} catch {
completed(.failure(.invalidData))
}
}
task.resume()
}
struct Results: Codable {
let slug, companyName, title, resultsDescription: String
let remote: Bool
let url: String
let tags, jobTypes: [String]
let location: String
let createdAt: Int
enum CodingKeys: String, CodingKey {
case slug
case companyName = "company_name"
case title
case resultsDescription = "description"
case remote, url, tags
case jobTypes = "job_types"
case location
case createdAt = "created_at"
}
}
I used the code in HomeViewController:
override func viewDidLoad() {
super.viewDidLoad()
title = "Home"
collectionView.backgroundColor = UIColor(named: "backgroundMain")
collectionView.register(SearchViewCell.self, forCellWithReuseIdentifier: cellId)
setupSearchBar()
Service.shared.getResults() { [weak self] result in
switch result {
case .success(let results):
print(results)
self?.jobResults = results
DispatchQueue.main.async {
self?.collectionView.reloadData()
}
case .failure(let error):
print(error)
}
}
}
888
I don't know what is wrong with my code. Can anyone help? Thanks!
You are discarding all meaningful error information, which will make this hard to diagnose. If you get an Error object, you should return that:
enum WebServiceError: Error {
case httpError(Data, Int)
}
func getResults(completion: #escaping (Result<[Results], Error>) -> Void) {
let urlString = "https://www.arbeitnow.com/api/job-board-api"
guard let url = URL(string: urlString) else {
completion(.failure(URLError(.badURL)))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse,
error == nil
else {
completion(.failure(error ?? URLError(.badServerResponse)))
return
}
guard 200 ..< 300 ~= response.statusCode else {
completion(.failure(WebServiceError.httpError(data, response.statusCode)))
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let results = try decoder.decode([Results].self, from: data)
completion(.success(results.data))
} catch {
completion(.failure(error))
}
}
task.resume()
}
So, that will,
if there was a URLSession error, tell you what the error was;
if there was a non-2xx status code, tell you what the code was (and return the body of the response, too, in case you want to look at that); and
if there was a parsing error, tell you what the parsing error was.
Without something like this, that captures the salient error information, you are flying blind.
In this case, the error is that you are parsing for [Results], but the structure is a dictionary, whose key is data and whose value is a [Results]. You are missing an object for this dictionary that wraps the [Results].
struct ResponseObject: Decodable {
let data: [Posting]
let links: Links
let meta: Meta
}
struct Posting: Decodable {
let slug, companyName, title, description: String
let remote: Bool
let url: String
let tags, jobTypes: [String]
let location: String
let createdAt: Int
}
struct Links: Decodable {
let first: URL?
let last: URL?
let prev: URL?
let next: URL?
}
struct Meta: Decodable {
let currentPage: Int
let path: URL
let perPage: Int
let from: Int
let to: Int
let terms: String
let info: String
}
func getResults(completion: #escaping (Result<[Posting], Error>) -> Void) {
let urlString = "https://www.arbeitnow.com/api/job-board-api"
guard let url = URL(string: urlString) else {
completion(.failure(URLError(.badURL)))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse,
error == nil
else {
completion(.failure(error ?? URLError(.badServerResponse)))
return
}
guard 200 ..< 300 ~= response.statusCode else {
completion(.failure(WebServiceError.httpError(data, response.statusCode)))
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let results = try decoder.decode(ResponseObject.self, from: data)
completion(.success(results.data))
} catch {
completion(.failure(error))
}
}
task.resume()
}
Your model does not match the JSON you receive from the link you provided:
Using:
struct Root: Codable{
let data: [WorkData]
let links: Links
let meta: Meta
}
// MARK: - Links
struct Links: Codable {
let first: String
let last, prev: String?
let next: String
}
// MARK: - Meta
struct Meta: Codable {
let currentPage, from: Int
let path: String
let perPage, to: Int
let terms, info: String
enum CodingKeys: String, CodingKey {
case currentPage = "current_page"
case from, path
case perPage = "per_page"
case to, terms, info
}
}
struct WorkData: Codable {
let slug, companyName, title, payloadDescription: String
let remote: Bool
let url: String
let tags, jobTypes: [String]
let location: String
let createdAt: Int
enum CodingKeys: String, CodingKey {
case slug
case companyName = "company_name"
case title
case payloadDescription = "description"
case remote, url, tags
case jobTypes = "job_types"
case location
case createdAt = "created_at"
}
}
should solve the problem
Usage:
let root = JsonDecoder().decode(Root.self, from: data)
let firstCompany = root.data[0]
Edit to adress the comment:
This should work!
let results = try decoder.decode([Results].self, from: data)
is your code isnĀ“t it?
instead use:
let root = JsonDecoder().decode(Root.self, from: data)
how could data be missing here?
After that you should either map the root object to your Result type to keep your Viewmodel and completion Handler the way they are now. Or change Viewmodel and completion Handler instead.
I'm having a hard time understanding how JSON data is supposed to be updated in a List in SwiftUI. I'm fetching data from NewsAPI.org, my list and detail views work just fine. I'm trying to figure out how to keep the list up-to-date when my json data changes, but the data remains outdated. I'm still a beginner to swift so if I made a mistake any help would be greatly appreciated.
UPDATED
Attempted to use combine with the same results, outdated data
New data class
class NewsData: ObservableObject {
var objectWillChange = PassthroughSubject<NewsData, Never>()
#Published var articles = [Article]() {
willSet {
objectWillChange.send(self)
}
}
init() {
guard let url = URL(string: "http://newsapi.org/v2/top-headlines?country=us&apiKey=API_KEY") else { 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(News.self, from: data) {
DispatchQueue.main.async() {
self.articles = response.articles
}
}
}
}
.resume()
}
/*
init() {
URLSession.shared
.dataTaskPublisher(for: URLRequest(url: URL(string: "http://newsapi.org/v2/top-headlines?country=us&apiKey=API_KEY")!))
.map(\.data)
.decode(type: News.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}, receiveValue: { data in
self.articles = data.articles
})
.store(in: &self.cancellables)
}
*/
/*
init() {
load()
}
func load() {
guard let url = URL(string: "http://newsapi.org/v2/top-headlines?country=us&apiKey=API_KEY") else { 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(News.self, from: data) {
DispatchQueue.main.async() {
self.articles = response.articles
}
}
}
}
.resume()
}
*/
}
My data old class
struct News : Codable {
var articles : [Article]
}
struct Article : Codable {
let description : String?
let title : String?
let author: String?
let source: Source
let content: String?
let publishedAt: String?
}
struct Source: Codable {
let name: String?
}
class NewsData: ObservableObject {
#Published var news: News = News(articles: [])
init() {
load()
}
func load() {
guard let url = URL(string: "http://newsapi.org/v2/top-headlines?country=us&apiKey=API_KEY_HERE") else { 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(News.self, from: data) {
DispatchQueue.main.async() {
self.news = response
}
}
}
}
.resume()
}
}
My ContentView
func relativeDate(date: String) -> String {
let formatter = RelativeDateTimeFormatter()
let dateFormatter = ISO8601DateFormatter()
return formatter.localizedString(for: dateFormatter.date(from: date) ?? Date(), relativeTo: Date())
}
struct ContentView: View {
#ObservedObject var news: NewsData
var body: some View {
NavigationView {
List(news.news.articles , id: \.title) { article in
VStack (alignment: .leading, spacing: 5){
Text(article.title ?? "")
.fontWeight(.bold)
.font(.subheadline)
.lineLimit(1)
Text(article.description ?? "")
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(1)
Text(relativeDate(date: article.publishedAt ?? ""))
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.navigationTitle("News")
}
}
}
SwiftUI is supposed to simplify things- I am bit frustrated as I have been working on the URLSession+JSONDecoder for weeks, I really need some help!
I have a function to load JSON data from a file in Swift and it works as expected. I copy/pasted the function and updated it to get the data via an API, however I receive a compile time error: "Unexpected non-void return value in void function". Is my approach wrong to use a function for JSON over the web?
JSON response:
{
"T":"CSU",
"v":468303,
"vw":1.2838,
"o":1.31,
"c":1.24,
"h":1.38,
"l":1.2001,
"t":1607374800000,
"n":994
}
struct Root2: Codable {
var T: String
var v: Double
var vw: Double
var o: String
var c: String
var h: Double
var l: Double
var t: Double
}
This file-based function works as expected:
let symbolData: [Root2] = load("symbolData.json")
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
For the web version, I receive compile time error: "Unexpected non-void return value in void function".
Line: return try decoder.decode(T.self, from: data)
func loadURL<T: Decodable>() -> T {
guard let url = URL(string: """)
else {
fatalError("Invalid URL in main bundle.")
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
do {
if let data = data {
let stringData = String(decoding: data, as: UTF8.self)
print("1 Fetched: \(url)")
print("2 Response: \(stringData)")
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
}
catch {
fatalError("Couldn't parse as :\n\(error)")
}
}.resume()
}
Working version after Leo's help!
class Manager: ObservableObject {
#Published var symbols: [Symbol] = []
func loadURL<T: Decodable>(using decoder: JSONDecoder = .msSince1970, completion: #escaping ((T?, Error?) -> Void)) {
let url = URL(string: """)!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
print("ops")
completion(nil, error)
return
}
print("1 Fetched: \(url)")
print("2 Response:", String(data: data, encoding: .utf8) ?? "")
_ = Data("""
[
{
"open": {
"price": 124.02,
"time": 1657105851499
},
"close": {
"price": 124.96,
"time": 1618647822184
},
"high": 124.64,
"low": 124.65,
"volume": 75665274,
"symbol": "AAPL"
}
]
""".utf8)
do {
completion(try decoder.decode(T.self, from: data), nil)
//completion(try decoder.decode(T.self, from: tempDataForTesting), nil)
} catch {
completion(nil, error)
}
}.resume()
}
}
extension JSONDecoder {
static let msSince1970: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
return decoder
}()
}
You can't wait for an asynchronous task to finish and return the result. What you need is a completion handler. You would need also to explicitly set the resulting type if you don't pass the resulting type to your decode method and you need to call resume to start your url session data task:
import SwiftUI
struct ContentView: View {
#ObservedObject var manager = Manager()
#State var string: String = "Hello, world!"
var body: some View {
Text(manager.symbol)
.padding()
.onAppear {
manager.load(symbol: manager.symbol) { (symbols: [Symbol]?, error: Error?) in
guard let symbols = symbols else {
print("error:", error ?? "")
string = "JSON could not be parsed"
return
}
for symbol in symbols {
print(symbol.open.price)
print(symbol.open.time)
print(symbol.close.price)
print(symbol.close.time)
print(symbol.high)
print(symbol.low)
print(symbol.volume)
print(symbol.symbol)
DispatchQueue.main.async {
manager.symbols = symbols
}
}
string = "JSON was successufly parsed"
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Manager: ObservableObject {
#Published var symbols: [Symbol] = []
#Published var symbol: String = "IBM"
func load<T: Decodable>(symbol: String, using decoder: JSONDecoder = .msSince1970, completion: #escaping ((T?, Error?) -> Void)) {
guard let url = URLComponents(symbol: symbol).url else {
completion(nil, URL.Error.invalidURL)
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
print("ops")
completion(nil, error)
return
}
print("1 Fetched: \(url)")
print("2 Symbol: \(symbol)")
print("3 Response:", String(data: data, encoding: .utf8) ?? "")
do {
completion(try decoder.decode(T.self, from: data), nil)
} catch {
completion(nil, error)
}
}.resume()
}
}
struct Symbol: Codable {
let open, close: Price
let high, low: Double
let volume: Int
let symbol: String
}
struct Price: Codable {
let price: Double
let time: Date
}
extension JSONDecoder {
static let msSince1970: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
return decoder
}()
}
extension URLComponents {
init(scheme: String = "https",
host: String = "sandbox.iexapis.com",
path: String = "/stable/stock/market/ohlc",
symbol: String,
token: String = "YOUR_API_TOKEN") {
self.init()
self.scheme = scheme
self.host = host
self.path = path
self.queryItems = [URLQueryItem(name: "symbols", value: symbol),
URLQueryItem(name: "token", value: token)]
}
}
extension URL {
enum Error: String, Swift.Error {
case invalidURL = "Invalid URL"
}
}
This will print
1 Fetched: https://sandbox.iexapis.com/stable/stock/market/ohlc?symbols=IBM&token=YOUR_API_TOKEN
2 Symbol: IBM
3 Response: [{"open":{"price":128.9,"time":1636600302693},"close":{"price":131.44,"time":1662259300134},"high":132.517,"low":130.074,"volume":3403359,"symbol":"IBM"}]
128.9
2021-11-11 03:11:42 +0000
131.44
2022-09-04 02:41:40 +0000
132.517
130.074
3403359
IBM
i know this is common but couldn't find answers. I thought of using dispatchGroups but not sure how I could do
Requirement: API 1 gives id, I need to construct url with that id and call another API to fetch imageUrl and then build struct with imageUrl
struct Item {
let itemId: String?
let quantity: String?
let itemImageUrl: String?
}
struct InitialDetails: Codable {
let id: String
let quantity: String
}
struct ImagePathData: Codable {
let imageUrl: String
}
API 1:
{
items: [{
id: "1",
quantity: "10"
}]
}
API 2
{
itemImagePath: "https://itemizedUrl/fish.png"
}
Code
func fetchData() -> [Item] {
URLSession.shared.dataTask(with: url) { (data, response, error) in
var items: [Items] = []
let initialData = try JSONDecoder().decode([InitialDetails].self, from: data)
for info in initialData {
var imageUrlPath: String?
let imageDataUrl = "https://itemizedUrl.com/\(info.id)"
URLSession.shared.dataTask(with: imageDataUrl) { (data, response, error) in
imageUrlPath = try JSONDecoder().decode(ImagePathData.self, from data)
}
let item = Item(itemId: initialData.id,
quantity: initialData.quantity,
itemImageUrl: imageUrlPath)
items.append(item)
}
return items
}
}
I know this is not right..pls advice how I could achieve this scenario
You need a completion with a dispachGroup
func fetchData(completion:#escaping([Item]) -> ()) {
var items: [Items] = []
let g = DispatchGroup()
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
let initialData = try JSONDecoder().decode([InitialDetails].self, from: data)
for info in initialData {
g.enter()
let imageDataUrl = "https://itemizedUrl.com/\(info.id)"
URLSession.shared.dataTask(with: imageDataUrl) { (data, response, error) in
do {
let imageUrlPath = try JSONDecoder().decode(ImagePathData.self, from data)
let item = Item(itemId: initialData.id,
quantity: initialData.quantity,
itemImageUrl: imageUrlPath.imageUrl)
items.append(item)
g.leave()
} catch {
print(error)
}
}
}
} catch {
print(error)
}
g.notify(queue:.main) {
completion(items)
}
}
}
I'm trying to return some data from a URLRequest in Swift 4, and to do so I've added a completion handler to my function signature, just with a Bool for the time being. This is the function:
func getJson(completionHandler: #escaping (Bool) -> ()) {
let jsonUrlString = "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=f4bf2ee721031a344b84b0449cfdb589:1:73741808"
guard let url = URL(string: jsonUrlString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data, err == nil else {
print(err!)
return
}
do {
let response = try
JSONDecoder().decode(TopStoriesResponse.self, from: data)
print(response.results)
// Pass results into arrays (title, abstract, url, image)
completionHandler(true)
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let jsonErr {
print("Error serializing JSON", jsonErr)
}
}.resume()
}
and I call it in viewDidLoad like so:
getJson { (success) in
print("Success")
}
Nothing is printing to the console so I'm wondering if I am using the completion handler correctly? But ultimately I'd like to switch out the Bool and instead pass a few values out of the request, back into arrays that I have in my view controller.
These are the structs I'm using to grab the pieces of JSON that i want:
struct TopStoriesResponse: Decodable {
let status: String
let results: [Story]
}
struct Story: Decodable {
let title: String
let abstract: String
let url: String
}
And I'm ultimately trying to fill these arrays in my view controller with the parsed JSON so I can arrange them in a table view:
var headlines = [String]()
var abstracts = [String]()
var urls = [URL]()
EDIT: Full code in case I'm going wrong somewhere else: https://pastebin.com/r402GKej
try creating the struct TopStoriesResponse and Story seperately from the ViewController and add the Networking struct to load data from the API
struct TopStoriesResponse: Decodable {
let status: String
let copyright: String
let num_results: Int
let results: [Story]
}
struct Story: Decodable {
let title: String
let abstract: String
let url: String
}
struct Networking {
static func getJson(completionHandler: #escaping (Bool) -> ()) {
let jsonUrlString = "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=f4bf2ee721031a344b84b0449cfdb589:1:73741808"
guard let url = URL(string: jsonUrlString) else {
return
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data, error == nil else {
print(error!.localizedDescription)
return
}
do {
let response: TopStoriesResponse = try JSONDecoder().decode(TopStoriesResponse.self, from: data)
print(response.results.count)
completionHandler(true)
} catch {
print(error.localizedDescription)
completionHandler(false)
}
}.resume()
}
}
Now try calling Networking.getJson from the ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
func loadData() {
Networking.getJson { (result) in
print(result)
}
}
}