Use keys from JSON object to parse data and display as a list - swift

I have an API that returns a JSON object with keys:
http://acnhapi.com/v1/bugs
Since it's not an array, I'd like to understand how to traverse it. I am trying to get a list of the bug names "common butterfly" and "yellow butterfly" etc. by using their keys common_butterfly and yellow_butterfly etc.
I want to display the value of common_butterfly.name.name-USen, but for each bug. So my list view should ultimately be displayed as:
common butterfly
yellow butterfly
tiger butterfly
etc.
(Alphabetical would be a bonus)
data
import SwiftUI
struct Bugs: Codable, Identifiable {
let id = UUID()
var name: String
}
class FetchBugs: ObservableObject {
#Published var bugs = [Bugs]()
init() {
let url = URL(string: "http://acnhapi.com/v1/bugs")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let bugsData = data {
let decodedData = try JSONDecoder().decode([Bugs].self, from: bugsData)
DispatchQueue.main.async {
self.bugs = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
list
import SwiftUI
struct BugList: View {
#ObservedObject var fetch = FetchBugs()
var body: some View {
VStack {
List(fetch.bugs) { bug in
VStack(alignment: .leading) {
Text(bug.name)
}
}
}
}
}
struct BugList_Previews: PreviewProvider {
static var previews: some View {
BugList()
}
}

With this solution you can decode all your localised names:
struct Bug: Decodable, Identifiable {
enum CodingKeys: String, CodingKey { case name }
let id = UUID()
var localizedNames: [String: String] = [:]
var nameUSen: String {
localizedNames["name-USen"] ?? "error"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let names = try container.decode([String: String].self, forKey: .name)
for (key, value) in names {
localizedNames[key] = value
}
}
}
Use .sorted { $0.nameUSen < $1.nameUSen } to sort your data:
class FetchBugs: ObservableObject {
#Published var bugs = [Bug]()
init() {
let url = URL(string: "http://acnhapi.com/v1/bugs")!
URLSession.shared.dataTask(with: url) { data, response, error in
do {
if let bugsData = data {
let decodedData = try JSONDecoder().decode([String: Bug].self, from: bugsData)
DispatchQueue.main.async {
self.bugs = Array(decodedData.values).sorted { $0.nameUSen < $1.nameUSen }
}
} else {
print("No data")
}
} catch {
print(error)
}
}.resume()
}
}
And display the USen name:
struct BugList: View {
#ObservedObject var fetch = FetchBugs()
var body: some View {
VStack {
List(fetch.bugs) { bug in
VStack(alignment: .leading) {
Text(bug.nameUSen)
}
}
}
}
}
If you'd ever want to access any other name you can use:
bug.localizedNames["name-EUde"]!

Here's a playground that illustrates getting all bug's names in alphabetical order:
struct Bug: Decodable, Identifiable {
let id: Int
let name: Name
struct Name: Decodable {
let nameUSen: String
enum CodingKeys: String, CodingKey {
case nameUSen = "name-USen"
}
}
}
do {
let butterflies = try Data(contentsOf: URL(string: "http://acnhapi.com/v1/bugs")!)
let allBugs = try JSONDecoder().decode([String: Bug].self, from: butterflies)
let bugs = Array(allBugs.values.sorted { $0.name.nameUSen < $1.name.nameUSen })
bugs.forEach { print($0.name.nameUSen) }
} catch {
print(error)
}

Related

How to change the value of a var with a TextField SwiftUI

I was trying to make a weather api call, the api call needs to have a location. The location that I pass is a variable, but now I want to change the location value based on a TextField's input.
I made the apiKey shorter just for safety measures. There's more code, but it's not relevant.
I just need to know how to change the city variable that is on the WeatherClass using the TextField that is in the cityTextField View.
Thanks.
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
var city: String = ""
func fetch() {
let location = city
let apiKey = "AP8LUYMSTHZ"
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(location)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}//----------------------------------- End of fetch()
}
struct WeatherData: Decodable {
let resolvedAddress: String
let days: [WeatherDays]
}
struct WeatherDays: Hashable, Decodable {
let datetime: String
let tempmax: Double
let tempmin: Double
let description: String
}
struct cityTextField: View {
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city).frame(height:30).multilineTextAlignment(.center).background().cornerRadius(25).padding(.horizontal)
}
}
I already watched a lot of tutorials for similar things buts none of them really helped me.
Try this approach using minor modifications to
func fetch(_ city: String){...} to fetch the weather for the city in your
TextField using .onSubmit{...}
struct ContentView: View {
#StateObject var weatherModel = WeatherClass()
var body: some View {
VStack {
cityTextField(weatherModel: weatherModel)
}
}
}
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city)
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch(city) // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
func fetch(_ city: String) { // <-- here
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}
Alternatively, as suggested by synapticloop, you could use this approach:
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
var body: some View {
TextField("Search city", text: $weatherModel.city) // <-- here
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch() // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
#Published var city: String = "" // <-- here
func fetch() {
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}

Problem trying to add Search bar for simple SwiftUI app retrieving web data

I have a small project which is an extension of a Swift UI exercise making a web call to Github from Greg Lim's book Beginning Swift UI:
https://github.com/ethamoos/GitProbe
I’ve been using this to practise basic skills and to try and add other features that could be useful in a realworld app.
My main change from the initial exercise was to add the option to choose which user to lookup (this was previously hardcoded) and allow the user to enter this. Because this can return a lot of data I would now like to make the resulting List .searchable so that the user can filter the results.
I’ve been following this tutorial here:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-search-bar-to-filter-your-data
but I’ve realised that this is based upon the data being returned being Strings, and therefore the search is a string.
I am returning JSON decoded into a list of User data objects so a straight search does not work. I am assuming that I can adjust this to match a string search against my custom objects but I'm not sure how to do this.
To give you an idea of what I mean here is the code:
import SwiftUI
import URLImage
struct Result: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [User]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct User: Codable, Hashable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
}
}
class FetchUsers: ObservableObject {
#Published var users = [User]()
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
guard let url = urlComponents.url else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let data = data {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct ContentView: View {
#State var username: String = ""
var body: some View {
NavigationView {
Form {
Section {
Text("Enter user to search for")
TextField("Enter your username", text: $username).disableAutocorrection(true)
.autocapitalization(.none)
}
NavigationLink(destination: UserView(username: username)) {
Text("Show detail for \(username)")
}
}
}
}
}
struct UserView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
#State var searchText = ""
var body: some View {
List {
ForEach(fetchUsers.users, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}.onAppear {
self.fetchUsers.search(for: username)
}
.searchable(text: $searchText)
.navigationTitle("Users")
}
/// With suggestion added
/// The search results
private var searchResults: [User] {
if searchText.isEmpty {
return fetchUsers.users // your entire list of users if no search input
} else {
return fetchUsers.search(for: searchText) // calls your search method passing your search text
}
}
}
struct UserDetailView: View {
var user: User
var body: some View {
Form {
Text(user.login).font(.headline)
Text("Git iD = \(user.id)")
URLImage(URL(string:user.avatarURL)!){ image in
image.resizable().frame(width: 50, height: 50)
}
}
}
}
Any help with this would be much appreciated.
Your UserListView is not properly constructed. I don't see why you would need a ScrollView with an empty text inside? I removed that.
So I removed searchText from the View to the FetchUsers class so we can delay the server requests thus avoiding unnecessary multiple calls. Please adjust it to your needs (check Apple's Debounce documentation. Everything should work as expected now.
import Combine
class FetchUsers: ObservableObject {
#Published var users = [User]()
#Published var searchText = ""
var subscription: Set<AnyCancellable> = []
init() {
$searchText
.debounce(for: .milliseconds(500), scheduler: RunLoop.main) // debounces the string publisher, delaying requests and avoiding unnecessary calls.
.removeDuplicates()
.map({ (string) -> String? in
if string.count < 1 {
self.users = [] // cleans the list results when empty search
return nil
}
return string
}) // prevents sending numerous requests and sends nil if the count of the characters is less than 1.
.compactMap{ $0 } // removes the nil values
.sink { (_) in
//
} receiveValue: { [self] text in
search(for: text)
}.store(in: &subscription)
}
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user.lowercased())]
guard let url = urlComponents.url else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
guard error == nil else {
print("Error: \(error!.localizedDescription)")
return
}
guard let data = data else {
print("No data received")
return
}
do {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct UserListView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
var body: some View {
NavigationView {
List {
ForEach(fetchUsers.users, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}
.searchable(text: $fetchUsers.searchText) // we move the searchText to fetchUsers
.navigationTitle("Users")
}
}
}
I hope this helps! :)
In the end, I think I've figured this out - thanks to the suggestions from Andre.
I need to correctly filter my data and then return the remainder.
Here's the corrected (abridged) version:
import SwiftUI
import URLImage
struct Result: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [User]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct User: Codable, Hashable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
}
}
class FetchUsers: ObservableObject {
#Published var users = [User]()
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
guard let url = urlComponents.url else {
return
// print("error")
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let data = data {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct ContentView: View {
#State var username: String = ""
var body: some View {
NavigationView {
Form {
Section {
Text("Enter user to search for")
TextField("Enter your username", text: $username).disableAutocorrection(true)
.autocapitalization(.none)
}
NavigationLink(destination: UserView(username: username)) {
Text("Show detail for \(username)")
}
}
}
}
}
struct UserView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
#State var searchText = ""
var body: some View {
List {
ForEach(searchResults, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}.onAppear {
self.fetchUsers.search(for: username)
}
.searchable(text: $searchText)
.navigationTitle("Users")
}
var searchResults: [User] {
if searchText.isEmpty {
print("Search is empty")
return fetchUsers.users
} else {
print("Search has a value - is filtering")
return fetchUsers.users.filter { $0.login.contains(searchText) }
}
}
}
struct UserDetailView: View {
var user: User
var body: some View {
Form {
Text(user.login).font(.headline)
Text("Git iD = \(user.id)")
URLImage(URL(string:user.avatarURL)!){ image in
image.resizable().frame(width: 50, height: 50)
}
}
}
}

Blank Screen on Loading List in SwiftUI

I am trying to display a list in a view after a condition. If data is recieved from API than screen will load new view having list showing multiple fields. Here I am showing only one field in the list. The navigation code is working fine and data is also decoded however blank screen appears when clicking list button screen moves to next view but blank screen.
Here is the view in which I am showing list :
import SwiftUI
struct MyPriceList: View {
#StateObject var road = ListAPI()
List
{
ForEach(road.priceRoad)
{
road in
HStack{
Text(road.packageName)
.font(.system(size: 15))
.foregroundColor(.black)
}
}
}
}
struct MyPriceList_Previews: PreviewProvider {
static var previews: some View {
MyPriceList()
}
}
}
The following is the viewmodel in which I have decoded the JSON data and applied the navigation
import Foundation
class ListAPI : ObservableObject
{
#Published var priceRoad = [ResponseList]()
func getList()
{
// url building code //
let list = URLSession.shared.dataTask(with: urlRequest)
{
(data, response, error) in
if let error = error {
print("Error \(error)")
}
if let data = data
{
do
{
let jsonList = try JSONDecoder().decode(PriceList.self, from: data)
let panama = jsonList.response
for pan in panama
{
print(pan.packageName) //successfully printing
}
if jsonList.success==true
{
DispatchQueue.main.async
{
self.navigate = true
self.priceRoad = jsonList.response
}
}
else
{
DispatchQueue.main.async
{
self.errorMessage = jsonList.message
}
}
}
catch
{
print("error \(error)")
}
}
}
list.resume()
} }
This is the data model for Json
struct PriceList : Codable
{
let success: Bool
let message: String
let response: [ResponseList]
enum CodingKeys:String, CodingKey{
case response = "ResponseData"
case success = "IsSuccess"
case message = "Message"
}
}
struct ResponseList:Codable
{
let packageId: Int
let packageName: String
let price: Double
let discountedPrice: Double
let testType: String
let testPackageGroupId: Int?
let SampleType: [SampleTypeList]?
enum CodingKeys:String, CodingKey{
case packageId = "PackageId"
case packageName = "PackageName"
case price = "Price"
case discountedPrice = "DiscountedPrice"
case testType = "Type"
case testPackageGroupId = "TestPackageGroupId"
case SampleType = "SampleTypeList"
}}
struct SampleTypeList:Codable
{
let testSampleTypeId: String
let sampleName: String
let colourCode: String
enum CodingKeys:String, CodingKey{
case testSampleTypeId = "TestSampleTypeId"
case sampleName = "SampleName"
case colourCode = "ColourCode"
}
}
I need to display TestName as packageName, MRP as price, B2B as discountedPrice, and TestType as testType.
try this approach, calling road.getList() in .onAppear{}:
struct MyPriceList: View {
#StateObject var road = ListAPI()
var body: some View { // <-- here need a body
List
{
ForEach(road.priceRoad)
{
road in
HStack{
Text(road.packageName)
.font(.system(size: 15))
.foregroundColor(.black)
}
}
}
.onAppear {
road.getList() // <-- here load your data
}
}
}
and make PriceList and ResponseList Identifiable, like this:
struct PriceList : Identifiable, Codable {
let id = UUID()
// ...
}
struct ResponseList: Identifiable, Codable {
let id = UUID()
// ...
}
struct SampleTypeList: Identifiable, Codable {
let id = UUID()
// ...
}
Alternatively, in ListAPI, you could have init() { getList() }, instead of using .onAppear {road.getList()}

Cannot map protocol compliant elements to generic elements

As you can see below, I downloaded an array of structures containing heterogeneous objects that were decoded into enums containing nested objects.
I would now like to put said objects into a generic Model structure, but the compiler won't allow this - the error is described below in the code comment. I am relatively new to programming in Swift, I would appreciate your help.
import Foundation
let jsonString = """
{
"data":[
{
"type":"league",
"info":{
"name":"NBA",
"sport":"Basketball",
"website":"https://nba.com/"
}
},
{
"type":"player",
"info":{
"name":"Kawhi Leonard",
"position":"Small Forward",
"picture":"https://i.ibb.co/b5sGk6L/40a233a203be2a30e6d50501a73d3a0a8ccc131fv2-128.jpg"
}
},
{
"type":"team",
"info":{
"name":"Los Angeles Clippers",
"state":"California",
"logo":"https://logos-download.com/wp-content/uploads/2016/04/LA_Clippers_logo_logotype_emblem.png"
}
}
]
}
"""
struct Response: Decodable {
let data: [Datum]
}
struct League: Codable {
let name: String
let sport: String
let website: URL
}
extension League: Displayable {
var text: String { name }
var image: URL { website }
}
struct Player: Codable {
let name: String
let position: String
let picture: URL
}
extension Player: Displayable {
var text: String { name }
var image: URL { picture }
}
struct Team: Codable {
let name: String
let state: String
let logo: URL
}
extension Team: Displayable {
var text: String { name }
var image: URL { logo }
}
enum Datum: Decodable {
case league(League)
case player(Player)
case team(Team)
enum DatumType: String, Decodable {
case league
case player
case team
}
private enum CodingKeys : String, CodingKey { case type, info }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(DatumType.self, forKey: .type)
switch type {
case .league:
let item = try container.decode(League.self, forKey: .info)
self = .league(item)
case .player:
let item = try container.decode(Player.self, forKey: .info)
self = .player(item)
case .team:
let item = try container.decode(Team.self, forKey: .info)
self = .team(item)
}
}
}
protocol Displayable {
var text: String { get }
var image: URL { get }
}
struct Model<T: Displayable> {
let text: String
let image: URL
init(item: T) {
self.text = item.text
self.image = item.image
}
}
do {
let response = try JSONDecoder().decode(Response.self, from: Data(jsonString.utf8))
let items = response.data
let models = items.map { (item) -> Model<Displayable> in // error: only struct/enum/class types can conform to protocols
switch item {
case .league(let league):
return Model(item: league)
case .player(let player):
return Model(item: player)
case .team(let team):
return Model(item: team)
}
}
} catch {
print(error)
}
You do not need generics here.
Change Model to accept any type that conforms to Displayable in the init
struct Model {
let text: String
let image: URL
init(item: Displayable) {
self.text = item.text
self.image = item.image
}
}
and then change the closure to return Model
let models = items.map { (item) -> Model in
If you want to keep your Model struct generic then you need to change the map call to
let models: [Any] = items.map { item -> Any in
switch item {
case .league(let league):
return Model(item: league)
case .player(let player):
return Model(item: player)
case .team(let team):
return Model(item: team)
}
}
This will give the following output when conforming to CustomStringConvertible
extension Model: CustomStringConvertible {
var description: String {
"\(text) type:\(type(of: self))"
}
}
print(models)
[NBA type:Model<League>, Kawhi Leonard type:Model<Player>, Los Angeles Clippers type:Model<Team>]
If you are only interested in name and the key representing the URL parse the JSON directly into Model by using nestedContainer this way
struct Response: Decodable {
let data: [Model]
}
struct Model: Decodable {
let name: String
let image: URL
enum DatumType: String, Decodable {
case league
case player
case team
}
private enum CodingKeys : String, CodingKey { case type, info }
private enum CodingSubKeys : String, CodingKey { case name, website, picture, logo }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(DatumType.self, forKey: .type)
let subContainer = try container.nestedContainer(keyedBy: CodingSubKeys.self, forKey: .info)
self.name = try subContainer.decode(String.self, forKey: .name)
let urlKey : CodingSubKeys
switch type {
case .league: urlKey = .website
case .player: urlKey = .picture
case .team: urlKey = .logo
}
self.image = try subContainer.decode(URL.self, forKey: urlKey)
}
}
do {
let response = try JSONDecoder().decode(Response.self, from: Data(jsonString.utf8))
let data = response.data
print(data)
} catch {
print(error)
}

SwiftUI - Not able to render the jsonData

I am very new to SwiftUi and I am trying to view the json data and I am currently working on retreiving the weather data from the openweathermap.org which is a free api to retrieve current weather. I am getting Error parsing Weather Json message. I am not sure what I am doing wrong!! Any help would be greatly appreciated and I have been stuck on this for a day. I referred many blogs and tutorials on how to use the Published var and ObservableObject I am not able to fix the problem.
This is my swift file
struct WeatherData {
public var Id: Int
public var main: String
public var weather: [Weather]
public var icon: String
}
extension WeatherData: Decodable, Identifiable {
var id: Int {return Id}
}
struct WeatherView: View {
#ObservedObject var fetch = FetchWeather()
var body: some View {
VStack {
List(fetch.weatherData) {
wthr in
VStack(alignment: .leading){
Text("\(wthr.id)")
Text("\(wthr.weather[0].description)")
Text("\(wthr.icon)")
.font(.system(size:11))
.foregroundColor(Color.gray)
}
}
}
}
}
struct Weather: Decodable {
let description: String
}
struct WeatherView_Previews: PreviewProvider {
static var previews: some View {
WeatherView()
}
}
class FetchWeather: ObservableObject {
#Published var weatherData = [WeatherData] ()
init() {
load()
}
func load() {
let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=London&appid=myapikey")!
URLSession.shared.dataTask(with: url) {
(data, response, error) in
do {
if let wthData = data {
let decodedData = try JSONDecoder().decode([WeatherData].self, from: wthData)
DispatchQueue.main.sync {
self.weatherData = decodedData
}
}
else {
print("No json Data available")
}
}catch {
print("Error parsing Weather Json")
}
}.resume()
}
}
Try this code. I have signed up to get the api and corrected the Model, ViewModel and View accordingly. I have not added the image loader for icon strings.
import SwiftUI
struct Weather: Decodable{
var description: String
var icon :String
}
struct MainData: Decodable {
var temp: Double
var pressure: Int
var humidity: Int
var temp_min: Double
var temp_max: Double
}
struct WeatherData: Decodable, Identifiable {
var id: Int
var main: MainData
var weather: [Weather]
var name: String
}
struct WeatherView: View {
#ObservedObject var fetch = FetchWeather()
var body: some View {
VStack(alignment: .leading) {
Text("Current Weather").font(.title).padding()
List(fetch.weatherData) { wthr in
HStack {
VStack(alignment: .leading){
Text("\(wthr.name)")
Text("\(wthr.weather[0].description)")
.font(.system(size:11))
.foregroundColor(Color.gray)
}
Spacer()
VStack(alignment: .trailing){
Text("\(wthr.main.temp-273.15, specifier: "%.1f") ºC")
}
Text("\(wthr.weather[0].icon)") // Image from "https://openweathermap.org/img/w/\(wthr.weather[0].icon).png"
.foregroundColor(Color.gray)
}
}
}
}
}
class FetchWeather: ObservableObject {
#Published var weatherData = [WeatherData]()
private let baseURL = "https://api.openweathermap.org/data/2.5/weather?q="
private let cities = [ "London", "Mumbai", "New+york", "Vatican+City" ]
private let api = "&appid="+"e44ebeb18c332fff46ab956bb38f9e07"
init() {
for city in self.cities {
self.load(self.baseURL+city+self.api)
}
}
func load(_ urlString: String) {
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
if let wthData = data {
let decodedData = try JSONDecoder().decode(WeatherData.self, from: wthData)
DispatchQueue.main.sync {
self.weatherData.append(decodedData)
}
}
else {
print("No json Data available")
}
} catch let error as NSError{
print(error.localizedDescription)
}
}.resume()
} else {
print("Unable to decode URL")
}
}
}