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")
}
}
}
Related
I tried to store the text in a variable of API
in the class
I do not want to transfer the entire model, I want to transfer the text as it is here
class Api : ObservableObject{
#Published var title : String = ""
#Published var details : String = ""
func getDataModelApi () {
guard let url = URL(string: APIgetURL.demo) else { return }
var request = URLRequest(url: url)
let token = "38|xxxxx"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
do {
let dataModel = try JSONDecoder().decode([model].self, from: data)
for i in dataModel {
DispatchQueue.main.async {
self.title = i.title
self.details = i.details
}
}
} catch {
print("error: ", error)
}
}
.resume()
}
}
In the title variable, the value was stored successfully, but the display in the view does not get anything
struct ContentView: View {
#StateObject var model3 = Api()
var body: some View {
VStack {
Text(model3.title)
}
.onAppear() {
Api().getDataModelApi()
}
}
}
Here in getData, it shows the complete model and needs a link, which I want to access from getDataModelApi
#Published var models : [model] = []
func getData (url : String) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
let token = "38|xxx"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
do {
let dataModel = try JSONDecoder().decode([model].self, from: data)
DispatchQueue.main.async {
self.models = dataModel
}
} catch {
print("error: ", error)
}
}
.resume()
}
You are using two different instances of Api and you throw away the second one in onAppear.
Replace
.onAppear() {
Api().getDataModelApi()
}
With
.onAppear() {
model3.getDataModelApi()
}
I have data on from the api on my app
I am sending the new data from my site
I want to put a message or notification on the application about the arrival of new data for this API
What is the best way to do this task?
This is the data I got
struct VideoView_Msrhiat: View {
#StateObject var model = Api()
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(model.models) { item in
VStack {
Text(item.title)
}
}
}
.onAppear() {
model.getData(url: APIgetURL.Tap1)
}
}
}
Also
struct model : Identifiable, Codable {
let id = UUID()
var color : String?
var details : String
}
class Api : ObservableObject{
#Published var models : [model] = []
func getData (url : String) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
let token = "38|Xxxx"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
do {
let dataModel = try JSONDecoder().decode([model].self, from: data)
DispatchQueue.main.async {
self.models = dataModel
} catch {
print("error: ", error)
}
}
resume()
}
}
{
"data":{
"email":"ms.lightwave#example.com",
"password":"123",
"token":""
}
}
struct JsonResult: View{
#State private var results = [GetData]()
var body: some View{
List(results, id: \.email){ item in
VStack(alignment: .leading) {
Text(item.password)
.font(.headline)
Text(item.token)
.font(.headline)
}
}.task {
await loadData()
}
}
struct Response : Codable {
var results: [GetData]
}
struct GetData: Codable{
var data : [Result]
}
struct Result: Codable {
var email: String
var password: String
var token: String
}
func loadData() async{
guard let url = URL(string: "MYURL") else {
print("invalid URL")
return
}
do{
let(data,_) = try await URLSession.shared.data(from: url)
// more code
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
{
results = decodedResponse.results
}
} catch {
print("Invalid Data")
}
}
}
i need to know if the codable structure is right according to the structure of data i gave ? and also the fetching in list ? please i need help in the URLsession i am still new and i barely know about url alot !
i would be grateful if you help me ! thank you verrrry much !!!!
In the JSON there is no array involved (no [] at all).
The model corresponding to the JSON is
struct Response: Decodable {
let data : UserData
}
struct UserData: Decodable {
let email: String
let password: String
let token: String
}
So the data source cannot be declared as an array. To avoid an optional type create an enum with associated values indicating a state. The benefit is that you can show different views depending on the state
struct JsonResult: View {
enum LoadingState {
case idle, loading, loaded(UserData), failure(Error)
}
this is the rest of the struct, consider that there is no List either because UserData is a single object.
#State private var state : LoadingState = .idle
var body: some View {
VStack {
switch state {
case .idle: EmptyView()
case .loading: ProgressView()
case .loaded(let userData):
VStack(alignment: .leading) {
Text(userData.password)
.font(.headline)
Text(userData.email)
.font(.headline)
}
case .failure(let error): Text(error.localizedDescription)
}
}.task {
await loadData()
}
}
func loadData() async {
state = .loading
guard let url = URL(string: "MYURL") else {
state = .failure(URLError(.badURL))
return
}
do {
let (data,_) = try await URLSession.shared.data(from: url)
// more code
let decodedResponse = try JSONDecoder().decode(Response.self, from: data)
state = .loaded(decodedResponse.data)
} catch {
state = .failure(error)
print(error) // this shows the real DecodingError
}
}
}
struct Response : Decodable, Hashable {
var results: [GetData]
}
struct GetData: Decodable, Hashable{
var data : [Result]
}
struct Result: Decodable, Hashable {
var email: String
var password: String
var token: String
}
enum RequestError: Error {
case invalidURL
case missingData
}
class JsonResultViewModel: ObservableObject{
#Published var response = [Response]()
func performHTTPRequest(urlString: String) async throws{
guard let url = URL(string: urlString) else {throw RequestError.invalidURL}
guard let (data, resp) = try? await URLSession.shared.data(from: url) else{throw RequestError.invalidURL}
guard (resp as? HTTPURLResponse)?.statusCode == 200 else {throw RequestError.invalidURL}
let decoder = JSONDecoder()
guard let jsonResponse = try? decoder.decode([Response].self, from: data) else {throw RequestError.missingData}
DispatchQueue.main.async {
self.response = jsonResponse
}
}
}
struct ContentView: View {
#StateObject private var results = JsonResultViewModel()
var body: some View {
List(results.response.indices, id: \.self){ index in
VStack(alignment: .leading) {
Text(results.response[index].results[index].data[index].email)
.font(.headline)
Text(results.response[index].results[index].data[index].token)
.font(.headline)
}
}
.onAppear(perform: {
Task{
do {
try await results.performHTTPRequest(urlString: "wwww.url.com")
} catch RequestError.invalidURL{
print("invalid URL")
} catch RequestError.missingData{
print("missing data")
}
}
})
}
}
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).
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
My API return a JSON data with 2 possible data set formats and how do I decode it? Thank you so much in advance
result.json
format 1:
{
product_name: "coffee"
product_code: "100010"
variant: [
flavour: [
{
flavour_name: "chocolate",
flavour_code: "C001",
},
{
flavour_name: "vanilla",
flavour_code: "C002",
}
],
size: [ { ... }, { ... } ]
]
}
format 2:
{
product_name: "bread",
product_code: "B123",
variant: [
portion: ["quarter", "half", "full"],
type: ["plain", "wheat", "oat"],
]
}
MenuViewModel.swift
class MenuDetailViewModel: ObservableObject, MenuDetailService {
var apiSession: APIService
#Published var detaildata: MenuDetailData?
var cancellables = Set<AnyCancellable>()
init(apiSession: APIService = APISession()) {
self.apiSession = apiSession
}
func getMenuDetail() {
let cancellable = self.getMenuDetail(menuCode: menuCode)
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("Handle error: \(error)")
case .finished:
break
}
}) { (detail) in
self.detaildata = detail.data
}
cancellables.insert(cancellable)
}
}
MenuDetailService.swift
protocol MenuDetailService {
var apiSession: APIService {get}
func getMenuDetail(menuCode: String) -> AnyPublisher<MenuDetailAPIResponse, APIError>
}
extension MenuDetailService {
func getMenuDetail(menuCode: String) -> AnyPublisher<MenuDetailAPIResponse, APIError> {
return apiSession.request(with: APIEndpoint.menuDetail(menuCode: menuCode))
.eraseToAnyPublisher()
}
}
APIService.swift
protocol APIService {
func request<T: Decodable>(with builder: RequestBuilder) -> AnyPublisher<T, APIError>
}
RequestBuilder.swift
protocol RequestBuilder {
var urlRequest: URLRequest {get}
}
APISession.swift
struct APISession: APIService {
func request<T>(with builder: RequestBuilder) -> AnyPublisher<T, APIError> where T: Decodable {
// 1
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// 2
return URLSession.shared
.dataTaskPublisher(for: builder.urlRequest)
// 3
.receive(on: DispatchQueue.main)
// 4
.mapError { _ in .unknown }
// 5
.flatMap { data, response -> AnyPublisher<T, APIError> in
if let response = response as? HTTPURLResponse {
// let dataString = String(data: data, encoding: .utf8) {
// print("Response data string:\n \(dataString)")
// }
if (200...299).contains(response.statusCode) {
print(String(data: data, encoding: .utf8) ?? "")
// 6
return Just(data)
.decode(type: T.self, decoder: decoder)
.mapError {_ in .decodingError}
.eraseToAnyPublisher()
} else {
// 7
return Fail(error: APIError.httpError(response.statusCode))
.eraseToAnyPublisher()
}
}
return Fail(error: APIError.unknown)
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
APIEndpoint.swift
enum APIEndpoint {
case menuDetail(menuCode: String)
}
extension APIEndpoint: RequestBuilder {
var urlRequest: URLRequest {
switch self {
case .menuDetail(let menuCode):
guard let url = URL(string: "API_URL/product/detail")
else {preconditionFailure("Invalid URL format")}
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(Constants.API_TOKEN, forHTTPHeaderField: "Authorization")
request.httpMethod = "POST"
let body: [String: Any] = ["brand": 1, "city": "", "menu_code": menuCode, "member_phone": ""]
let rb = try! JSONSerialization.data(withJSONObject: body)
request.httpBody = rb
return request
}
}
}
MenuDetailAPIResponse.swift
struct MenuDetailAPIResponse: Codable {
let data: MenuDetailData
}
MenuDetailData.swift
struct MenuDetailData: Codable, Identifiable {
let id = UUID()
let productName: String
let productCode: String
let variant: [MenuVariant]
}
struct MenuVariant: Codable, Identifiable, Hashable {
let id = UUID()
let flavour: [MenuFlavour]
let size: [MenuSize]
}
struct MenuFlavour: Codable, Identifiable, Hashable {
let id = UUID()
let flavourName: String
let flavourCode: String
}
try combine two models into one with optional variables:
struct MenuDetailData: Codable, Identifiable {
let id = UUID()
let productName: String
let productCode: String
let variant: [MenuVariant]
}
struct MenuVariant: Codable, Identifiable, Hashable {
let id = UUID()
let flavour: [MenuFlavour]?
let size: [MenuSize]?
let portion: [String]?
let type: [String]?
}
and after you can check received data
if let _ = model.type
if let _ = model.flavour...
etc.