I want to get Json from API, and use Codable protocol, but when I init published var, I get an error.
struct Search: Codable {
let result: [String]
}
class SearchViewModel: ObservableObject {
#Published var data = Search()
func loadData(search: String) {
var urlComps = URLComponents(string: getUrl)
let queryItems = [URLQueryItem(name: "result", value: search)]
urlComps!.queryItems = queryItems
let url = urlComps!.url!.absoluteString
guard let Url = URL(string: url) else { return }
URLSession.shared.dataTask(with: Url) { (data, res, err) in
do {
if let data = data {
let decoder = JSONDecoder()
let result = try decoder.decode(Search.self, from: data)
DispatchQueue.main.async {
self.data = result
}
} else {
print("there's no Dataš")
}
} catch (let error) {
print("Error!")
print(error.localizedDescription)
}
}.resume()
}
}
Change
#Published var data:Search?
Related
I thought my completionHandler returned a value of the type 'NewsAPIResponse', but I don't know how to assign it to a static var. Why is my code throwing an error? Is it not converting? I have checked all other areas of code for debugging. I am not sure whether the problem stems from how I created the completionHandler or not. Any insights would be greatly appreciated.
struct Article {
let site: String
let title: String
let date: Date
let tickers: String
let image: String
var siteText: String {
site
}
var titleText: String {
title
}
var tickerText: String {
tickers
}
var dateText: String {
"\(relativeDateFormatter.localizedString(for: date, relativeTo:Date()))"
}
var imageURL: URL? {
URL(string: image)
}
}
extension Article: Codable{}
extension Article: Identifiable{
var id: String{title}
}
struct NewsAPIResponse : Decodable{
let content: [Article]
}
extension Article {
func fetchData(completionHandler: #escaping (NewsAPIResponse) -> Void) {
let url = URL(string: "My API KEY")
var request = URLRequest(url: URL!)
request.addValue("application/json", forHTTPHeaderField: "Accept")
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print(error!)
return
}
guard let data = data else {
print("Data is empty")
return
}
let jsonDecoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
let apiResponse = try!jsonDecoder.decode(NewsAPIResponse.self, from: data)
completionHandler(apiResponse)
}
task.resume()
}
static var previewData: NewsAPIResponse = { //error is thrown here
let response = fetchData(){[weak self](apiResponse) in
}
return response
}
}
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 = []
}
}
}
}
I am following a tutorial on REST API calls with Swift and Codable. I cannot compile the following although I was careful when I typed all of it. Can anyone tell me what's wrong? Also, can anyone point me to a better tutorial? The error is:
Catch block is unreachable
and also
cannot find json in scope
import UIKit
import Foundation
struct Example: Codable {
let userId: Int
let id: Int
let title: String
let completed: Bool
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func getJson(completion: #escaping (Example)-> ()) {
let urlString = "https://jsonplaceholder.typicode.com/todos/1"
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) {data, res, err in
if let data = data {
let decoder = JSONDecoder()
do {
let json: Example = try! decoder.decode(Example.self, from: data)
completion(json)
}catch let error {
print(error.localizedDescription)
}
}
}.resume()
}
}
getJson() { (json) in
print(json.id)
}
}
struct Example: Decodable {
let userId: Int
let id: Int
let title: String
let completed: Bool
}
struct APIRequest {
var resourceURL: URL
let urlString = "https://jsonplaceholder.typicode.com/todos/1"
init() {
resourceURL = URL(string: urlString)!
}
//create method to get decode the json
func requestAPIInfo(completion: #escaping(Result<Example, Error>) -> Void) {
let dataTask = URLSession.shared.dataTask(with: resourceURL) { (data, response, error) in
guard error == nil else {
print (error!.localizedDescription)
print ("stuck in data task")
return
}
let decoder = JSONDecoder()
do {
let jsonData = try decoder.decode(Example.self, from: data!)
completion(.success(jsonData))
}
catch {
print ("an error in catch")
print (error)
}
}
dataTask.resume()
}
}
class ViewController: UIViewController {
let apiRequest = APIRequest()
override func viewDidLoad() {
super.viewDidLoad()
apiRequest.requestAPIInfo { (apiResult) in
print (apiResult)
}
}
}
Instead of "do catch", you can use "guard let"
func getJson(completion: #escaping (Example)-> ()) {
let urlString = "https://jsonplaceholder.typicode.com/todos/1"
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) {data, res, err in
guard let data = data else {return print("error with data")}
let decoder = JSONDecoder()
guard let json: Example = try? decoder.decode(Example.self, from: data) else {return print("error with json")}
completion(json)
}.resume()
}
}
This does NOT handle errors, so my solution is just to an answer to your question, not a general solution for every similar cases.
I tried to directly get the data recieved from a URLsession updated into the instance variables. Tried the code below in playgroud, I can see until the self.cityName = weatherdecoded.name the code seems working fine, but the self.cityName which intended to be the instance's variable didn't got updated. The results are nils. Hope to understand the reason, what is the mistake i made. Thanks!
import UIKit
class WeatherManager {
var cityName: String?
var conditionID: Int?
var temp: Double?
func fetchData(cityName: String) {
let session = URLSession(configuration: .default)
let urlStr = "https://api.openweathermap.org/data/2.5/weather?units=metric&appid=8da179fa1c83749056ec6a5385cabb04&q=" + cityName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
print(urlStr)
let url = URL(string: urlStr)!
let weatherDataSession = session.dataTask(with: url, completionHandler: getdata(data:response:error:))
weatherDataSession.resume()
}
func getdata(data: Data?, response: URLResponse?, error: Error?) {
if error != nil {
print(error!)
return
}
if let safedata = data {
let decoder = JSONDecoder()
do {
let weatherdecoded = try decoder.decode(Weatherdata.self, from: safedata)
self.cityName = weatherdecoded.name
self.conditionID = weatherdecoded.weather[0].id
self.temp = weatherdecoded.main.temp
} catch {
print(error)
}
}
}
}
struct Weatherdata: Decodable {
let weather : [Weather]
let main: Main
let name: String
}
struct Weather: Decodable {
let id: Int
let description: String
}
struct Main: Decodable {
let temp: Double
}
let weathermanager = WeatherManager()
weathermanager.fetchData(cityName: "beijing")
print(weathermanager.cityName)
print(weathermanager.conditionID)
print(weathermanager.temp)
The simple reason is the fact that you are printing the values before the request actually completes. You have to use a completion handler:
class WeatherManager {
func fetchObject<T: Decodable>(urlPath: String, onCompletion: #escaping (Result<T, Error>) -> Void) {
let session = URLSession(configuration: .default)
let url = URL(string: urlPath)!
let task = session.dataTask(with: url) { data, _, error in
if let error = error {
onCompletion(.failure(error))
return
}
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(T.self, from: data ?? Data())
onCompletion(.success(decoded))
} catch {
onCompletion(.failure(error))
}
}
task.resume()
}
func fetchWeather(cityName: String, onCompletion: #escaping (Result<Weatherdata, Error>) -> Void) {
let urlPath = "https://api.openweathermap.org/data/2.5/weather?units=metric&appid=8da179fa1c83749056ec6a5385cabb04&q=" + cityName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
fetchObject(urlPath: urlPath, onCompletion: onCompletion)
}
}
struct Weatherdata: Decodable {
let weather : [Weather]
let main: Main
let name: String
}
struct Weather: Decodable {
let id: Int
let description: String
}
struct Main: Decodable {
let temp: Double
}
let weathermanager = WeatherManager()
weathermanager.fetchWeather(cityName: "beijing") { result in
switch result {
case .success(let weatherData):
print(weatherData.name)
print(weatherData.weather[0].id)
print(weatherData.main.temp)
case .failure(let error):
print(error)
}
}
I'm having a problem where sometimes I can't successfully decode a json file due to a slow networking call. I have a list of stocks and I'm trying to successfully decode each one before the next stock gets decoded.
For example, I would have a list of 4 stocks but 3 would be successful decoded but 1 won't. The one that fails is also random. When I print out the url for the one that fails, the url and json file is correct yet I get an error of it not reading because its on a wrong format.
The list of stocks are retrieved through Firebase and after I receive them, I have a completion handler that tries to make a network call to the server. The reason why I added Firestore code here is because when I put a stop point at when case is successful, I notice that its hitting Thread 5 out of 14 Threads. Is having this many threads common? I know it's a threading issue but am having such a huge problem identifying where I should do dispatchGroups. Any help and clarifications would be much appreciated!
APIManager
private var stocks = [Stock]()
func getStockList( for symbols: [Stock], completion: ((Result<[Stock]>) -> Void)?) {
let dispatchGroup = DispatchGroup()
for symbol in symbols {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = APIManager.baseAPIURL
urlComponents.path = "\(APIManager.baseRelativePath)/market/batch"
//URL parameters
let token = URLQueryItem(name: "token", value: "fakeToken")
let symbolsItem = URLQueryItem(name: "symbols", value: symbol.symbol)
let typesItem = URLQueryItem(name: "types", value: "quote")
urlComponents.queryItems = [token, symbolsItem, typesItem]
guard let url = urlComponents.url else { fatalError("Could not create URL from components") }
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
dispatchGroup.enter()
let task = session.dataTask(with: request) { (responseData, response, err) in
guard err == nil else {
completion?(.failure(err!))
return
}
guard let jsonData = responseData else {
let err = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(err))
return
}
let decoder = JSONDecoder()
do {
let data = try decoder.decode([String: Stock].self, from: jsonData)
let jsonData = data.map{ $0.value }
completion?(.success(jsonData))
dispatchGroup.leave()
} catch {
completion?(.failure(error))
print("Failed to decode using stock URL for \(symbol.symbol ?? ""): \n \(url)")
}
}
task.resume()
}
}
HomeViewController
class HomeViewController: UIViewController, LoginViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
fetchStocksFromFireStore(completion: { (stocks) -> Void in
APIManager.shareInstance.getStockList(for: self.fetchedStocks) { (result) in
switch result {
case .success(let stocks):
stocks.forEach { (stock) in
print("Company: \(stock.companyName ?? "")")
}
case .failure(let error):
print(error.localizedDescription)
}
}
self.tableView.reloadData()
})
}
}
func fetchStocksFromFireStore(completion: #escaping ([Stock]) -> ()) {
let uid = Auth.auth().currentUser?.uid ?? ""
let db = Firestore.firestore()
db.collection("users").document(uid).collection("stocks").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting stocks snapshot", err.localizedDescription)
return
} else {
snapshot?.documents.forEach({ (snapshot) in
var stock = Stock()
stock.symbol = snapshot.documentID
self.fetchedStocks.append(stock)
})
completion(self.fetchedStocks)
}
}
}
Model
struct Stock {
var symbol: String?
var companyName: String?
var latestPrice: Double?
enum CodingKeys: String, CodingKey {
case symbol
case companyName
case latestPrice
}
enum QuoteKeys: String, CodingKey {
case quote
}
}
extension Stock: Encodable {
func encode(to encoder: Encoder) throws {
var quoteContainer = encoder.container(keyedBy: QuoteKeys.self)
var quoteNestedValues = quoteContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .quote)
try quoteNestedValues.encode(symbol, forKey: .symbol)
try quoteNestedValues.encode(companyName, forKey: .companyName)
try quoteNestedValues.encode(latestPrice, forKey: .latestPrice)
}
}
extension Stock: Decodable {
init(from decoder: Decoder) throws {
let quoteValues = try decoder.container(keyedBy: QuoteKeys.self)
let quoteNestedValues = try quoteValues.nestedContainer(keyedBy: CodingKeys.self, forKey: .quote)
symbol = try quoteNestedValues.decode(String.self, forKey: .symbol)
companyName = try quoteNestedValues.decode(String.self, forKey: .companyName)
latestPrice = try quoteNestedValues.decode(Double.self, forKey: .latestPrice)
}
}