How to represent a JSON file in SwiftUI? - ios

I'm quite a beginner and trying to get the OpenWeather API JSON to show up in my challenge project.
I managed to model it
struct WeatherRespose: Codable {
var weather: [Weather]
var name: String
}
&
import Foundation
struct Weather: Hashable, Codable {
var main: String
var description: String
}
In addition to fetch the data in ContentView. However, when I try to present it:
#State var weatherForcast = Weather() or #State var weatherForcast = WeatherResponse() I get this error: Missing argument for parameter 'from' in call, insert 'from: <#Decoder#>'
The only thing that worked for me is to present the data in an array:
#State var weatherForcast = [Weather]()
Any idea what am I missing? thank you so much! Ran

I made pretty simple example of how you can do this. There are several additional files, so it's easier to understand how it works in details:
Create additional file called NetworkService, it will fetch weather data:
import Foundation
final class NetworkService{
private let url = "https://example.com"
func performWeatherRequest(completion: #escaping (Result<WeatherResponse, Error>) -> Void){
URLSession.shared.dataTask(with: URL(string: url)!) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(WeatherError.failedToDownload))
return
}
let weatherResponse: WeatherResponse = try! JSONDecoder().decode(WeatherResponse.self, from: data)
completion(.success(weatherResponse))
}.resume()
}
public enum WeatherError: Error {
case failedToDownload
}
}
Create simple ViewModel which will retrieve data from our NetworkService and prepare to present it in ContentView
import Foundation
import SwiftUI
extension ContentView {
#MainActor class ContentViewVM: ObservableObject {
private var networkService = NetworkService()
#Published var currentWeatherMain: String?
#Published var currentWeatherDescription: String?
func fetchWeather(){
networkService.performWeatherRequest { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let weatherResponse):
self?.currentWeatherMain = weatherResponse.weather[0].main
self?.currentWeatherDescription = weatherResponse.weather[0].description
case .failure(_):
print("oops, error occurred")
}
}
}
}
}
}
Add our ContentViewVM to our ContentView:
import SwiftUI
struct ContentView: View {
#StateObject var viewModel = ContentViewVM()
var body: some View {
VStack {
Text("The main is: \(viewModel.currentWeatherMain ?? "")")
Text("The description is: \(viewModel.currentWeatherDescription ?? "")")
}
.onAppear{
viewModel.fetchWeather()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Hope it helps.

Related

How to navigate between two API parameters in SwiftUI

I'm beginner of iOS app development, currently doing iOS & Swift Bootcamp on Udemy by Angela Yu. I have this app called H4X0R News, which shows Hacker News all stories that are on the front/home page on the app by using its API. By the end of a module the app works fine when url property from API json is not nil but there are certain cases when url equals nil. These are posts which instead has story_text property. So what I want here to adjust is add story_text to my code and use it to navigate between this and url parameter. Here's the code I've got:
ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List(networkManager.posts) { post in
NavigationLink(destination: DetailView(url: post.url)) {
HStack {
Text(String(post.points))
Text(post.title)
}
}
}
.navigationTitle("H4X0R NEWS")
}
.onAppear {
self.networkManager.fechData()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
WebView.swift
import Foundation
import WebKit
import SwiftUI
struct WebView: UIViewRepresentable {
let urlString: String?
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if let safeString = urlString {
if let url = URL(string: safeString) {
let request = URLRequest(url: url)
uiView.load(request)
}
}
}
}
DetailView.swift
import SwiftUI
struct DetailView: View {
let url: String?
var body: some View {
WebView(urlString: url)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(url: "https://www.google.com/")
}
}
NetworkManager.swift
import Foundation
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
func fechData() {
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.hits
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
PostData.swift
import Foundation
struct Results: Decodable {
let hits: [Post]
}
struct Post: Decodable, Identifiable {
var id: String {
return objectID
}
let objectID: String
let points: Int
let title: String
let url: String?
}
So what I'm sure I need to add this story_text to PostData as String? and then make conditional statement in the WebView.updateUIView() function. Then update the code in other files. But like I said, I'm new in the programming world and I seek for help here for the first time since I've started the course.
If I understand you correct, you want to navigate to a simple text view if there is no URL for the Post?
So you can do it like this:
let text: String?
var body: some View {
if let url = url {
WebView(urlString: url)
} else {
Text(text ?? "-")
}
}

Build and update lists automatically with SwiftUI and Combine

I'm starting to learn SwiftUI development, I'm making my first basic SwiftUI based news application which I plan on open sourcing but I'm currently stuck. I've been reading Apple's documentation and looking at examples on how to automatically handle data changes in SwiftUI using combine etc. I've found an article, that's suppose to automatically update the list. I haven't been able to see any immediate data changes or anything being logged.
I'm using the same structure as NewsAPI but as an example I've uploaded it to a GitHub repo. I've made a small project and tried updating the data in my repo and trying to see any changes made in my data. I'm honestly trying my best and could really use some pointers or corrections in what my errors may be. I think my confusion lies in #ObservedObject and #Published and how to handle any changes in my content view. The article doesn't show anything they did to handle data changes so maybe I'm missing something?
import Foundation
import Combine
struct News : Codable {
var articles : [Article]
}
struct Article : Codable,Hashable {
let description : String?
let title : String?
let author: String?
let source: Source
let content: String?
let publishedAt: String?
}
struct Source: Codable,Hashable {
let name: String?
}
class NewsData: ObservableObject {
#Published var news: News = News(articles: [])
init() {
guard let url = URL(string: "https://raw.githubusercontent.com/ca13ra1/data/main/data.json") 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
print("data called")
}
}
}
}
.resume()
}
}
My View
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var data: NewsData
var body: some View {
List(data.news.articles , id: \.self) { article in
Text(article.title ?? "")
}
}
}
The data binding in SwiftUI does no extend to synching state with the server. If thats what you want then you need to use some other mechanism to tell the client that there is new data in the server (ie GraphQL, Firebase, send a push, use a web socket, or poll the server).
A simple polling solution would look like this and note you should not be doing network requests in init, only when you get an on appear from the view because SwiftUI eager hydrates its views even when you cannot see them. Similarly you need to cancel polling when you are off screen:
struct Article: Codable, Identifiable {
var id: String
}
final class ViewModel: ObservableObject {
#Published private(set) var articles: [Article] = []
private let refreshSubject = PassthroughSubject<Void, Never>()
private var timerSubscription: AnyCancellable?
init() {
refreshSubject
.map {
URLSession
.shared
.dataTaskPublisher(for: URL(string: "someURL")!)
.map(\.data)
.decode(type: [Article].self, decoder: JSONDecoder())
.replaceError(with: [])
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.assign(to: &$articles)
}
func refresh() {
refreshSubject.send()
guard timerSubscription == nil else { return }
timerSubscription = Timer
.publish(every: 5, on: .main, in: .common)
.autoconnect()
.sink(receiveValue: { [refreshSubject] _ in
refreshSubject.send()
})
}
func onDisappear() {
timerSubscription = nil
}
}
struct MyView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
List(viewModel.articles) { article in
Text(article.id)
}
.onAppear { viewModel.refresh() }
.onDisappear { viewModel.onDisappear() }
}
}
I've found a nifty swift package which allows me to easily repeat network calls. It's called swift-request. Thanks to #pawello2222 for helping me solve my dilemma.
import Request
class NewsData: ObservableObject {
#Published var news: News = News(articles: [])
init() {
test()
}
func test() {
AnyRequest<News> {
Url("https://raw.githubusercontent.com/ca13ra1/data/main/data.json")
}
.onObject { data in
DispatchQueue.main.async() {
self.news = data
}
}
.update(every: 300)
.update(publisher: Timer.publish(every: 300, on: .main, in: .common).autoconnect())
.call()
}
}
It's now working as expected, probably the easier option.
Demo:

How do i call the parsed data from the GET request that have a longer nested JSON structure in Swift?

NOTE: Forgive my cluelessness, i am still new in regards to this. The full code is posted at the bottom.
ISSUE: It seems that when i have a short nest, i am able to call it for my #Published property however when i try an api request with a longer nest, like this. and type Decodable structs that follows the structure of the GET request
struct TripScheduleTest: Codable {
let TripList: InitialNest
}
struct InitialNest: Codable {
var Trip: [TravelDetail]
}
struct TravelDetail: Codable {
var Leg: [TripTest]
}
struct TripTest: Codable, Hashable {
var name: String
var type: String
}
I am not able to call it for the #Published var dataSet1 = [TripTest]()
self.dataSet1 = tripJSON.TripList.Trip.Leg
I get an error message, that says "Value of type '[TravelDetail]' has no member 'Leg'
I am not sure why, however it works when i use [TravelDetail]() instead of [TripTest]() in the #Published var and stop at Trip before Leg for the dataSet1, then it seems to at least build successfully. But now i am not able to get the name and type information from the request
Full code
import SwiftUI
struct TripScheduleTest: Codable {
let TripList: InitialNest
}
struct InitialNest: Codable {
var Trip: [TravelDetail]
}
struct TravelDetail: Codable {
var Leg: [TripTest]
}
struct TripTest: Codable, Hashable {
var name: String
var type: String
}
class TripViewModel: ObservableObject {
#Published var dataSet1 = [TripTest]()
init() {
let urlString = "http://xmlopen.rejseplanen.dk/bin/rest.exe/trip?originId=8600790&destId=6553&format=json"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, resp, err) in
guard let data = data else { return }
do {
let tripJSON = try
JSONDecoder().decode(TripScheduleTest.self, from: data)
print(data)
DispatchQueue.main.async {
self.dataSet1 = tripJSON.TripList.Trip.Leg
}
} catch {
print("JSON Decode error: ", error)
}
}.resume()
}
}
struct TripView: View {
#ObservedObject var vm = TripViewModel()
var body: some View {
List(vm.dataSet1, id: \.self) { day in
Text("Test")
.font(.system(size: 12, weight: .bold))
Text(" \(day.name)")
.font(.system(size: 12))
}
}
}
Trip is an array (note the [])
You need to get one item of the array by index for example
tripJSON.TripList.Trip.first?.Leg
To assign the value to a non-optional array write
self.dataSet1 = tripJSON.TripList.Trip.first?.Leg ?? []

How can I extract each data text from result table from JSON? :SwiftUI

first of all I made this NetworkManager class to made networking with json api. like this below.
import Foundation
import SwiftUI
import Combine
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
func fetchData() {
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "183.111.148.229"
urlComponents.path = "/mob_json/mob_json.aspx"
urlComponents.queryItems = [
URLQueryItem(name: "nm_sp", value: "UP_MOB_CHECK_LOGIN"),
URLQueryItem(name: "param", value: "1000|1000|1")
]
if let url = urlComponents.url {
print(url)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.Table
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
And this is my data Model which is structure for my json data. I also made this by refer code on the internet.
import Foundation
struct Results: Decodable {
let Table: [Post]
}
struct Post: Decodable, Identifiable {
var id: String {
return CD_FIRM
}
let CD_FIRM: String
let NM_FIRM: String
let CD_USER: String
let NM_USER: String
}
On the view, This is work fine I can see my result. But this is not what I want.
I want to see each single text value.
import SwiftUI
struct SwiftUIView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List(networkManager.posts) { post in
Text(post.NM_FIRM)
}
}.onAppear {
self.networkManager.fetchData()
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
** I want to extract single text value from results like this**
with JUST single text.
I can extract all datas from json, but I don't know how to extract each values from result.
I tried like this below
import SwiftUI
struct SwiftUIView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
VStack {
Text(networkManager.posts.NM_FIRM)
}.onAppear {
self.networkManager.fetchData()
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
But this one didn't work. where do I have to fix this? Please help me.
Add more thing
import Foundation
struct Results: Decodable {
let Table: [Post]
}
struct Post: Decodable, Identifiable {
var id: String {
return name
}
let name: String
let cellPhone: String
}
// I want to get value like this but this didn't work
var data1 = name
var data2 = cellPhone

Getting JSON from a public API to render in a list view in SwiftUI

I'm trying to get the data from a Rest Api to download and render in a list view in SwiftUI.
I think I manage to get the JSON to download and assign to all the relevant Structs correctly but nothing displays in the list view on the simulator when I go to build it.
I'm not even sure I need to have the 'Enum CodingKeys in there.
Can anyone point out where I may be going wrong?
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
VStack {
List(fetcher.launches) { launch in
VStack (alignment: .leading) {
Text(launch.mission_name)
Text(launch.details)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class LaunchDataFetcher: ObservableObject {
#Published var launches = [launch]()
init(){
load()
}
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String
enum CodingKeys: String, CodingKey {
case flight_number = "flight_number"
case mission_name = "mission_name"
case details = "details"
}
}
// Now conform to Identifiable
extension launch: Identifiable {
var id: Int { return flight_number }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First of all, I try to find which error of your code and recognize the launch.details of response data somewhere have null. So that I just mark this property Optional and it work.
For more details, you can refer below code :
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String?
enum CodingKeys: String, CodingKey {
case flight_number = "flight_number"
case mission_name = "mission_name"
case details = "details"
}
}
At catch line, get the error message to know exactly what's happen
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
print(decodedLists)
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch let parsingError {
print ("Error", parsingError)
}
}.resume()
}
Hope this help!
Any messages in console? I think you need to add NSAppTransportSecurity>NSAllowsArbitraryLoads>YES in your .plist
Transport security has blocked a cleartext HTTP
With the help from Trai Nguyen, I managed to get the simulator to display the data.
All I was missing was to ensure the variable named 'details' had an optional property (by adding on the "?") - like so:
public var details: String?
and then making sure when I render the text in my view, I had to give it a value to insert if the return is null - like so:
Text(launch.details ?? "No Data Found")
Here is the complete code that works for me:
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
VStack {
List(fetcher.launches) { launch in
VStack (alignment: .leading) {
Text(launch.mission_name)
Text(launch.details ?? "No Data Found")
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class LaunchDataFetcher: ObservableObject {
#Published var launches = [launch]()
init(){
load()
}
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
print(decodedLists)
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch let parsingError {
print ("Error", parsingError)
}
}.resume()
}
}
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String?
}
/// Now conform to Identifiable
extension launch: Identifiable {
var id: Int { return flight_number }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources