I’m having a brain block right now and I can’t figure out how to display only 12 hours of hourly temperature rather than the 48 hours with the OpenWeatherMap API OneCall. I've tried messing around with the for-in loop but not having any luck.
Here is my ViewModel
class WeatherModel: ObservableObject {
//Declare published property wrapper, that when the the property value changes, we want to notifty anyone who is observing it
#Published var weatherData: Weather?
#AppStorage("cityName") var cityName = ""
init(){
getWeatherData(cityName)
}
//Init method gets run when a new instance of WeatherModel is created
//MARK: - OpenWeatherMap API methods
func getWeatherData(_ cityName: String){
CLGeocoder().geocodeAddressString(cityName){(placemarks, error ) in
if let error = error {
print(error)
}
if let lat = placemarks?.first?.location?.coordinate.latitude,
let lon = placemarks?.first?.location?.coordinate.longitude {
//first is the first element of the collection
let weatherUrlString = "https://api.openweathermap.org/data/2.5/onecall?lat=\(lat)&lon=\(lon)&exclude=minutely,daily,alerts&units=imperial&appid=\(Constants.apiKey)"
let weatherUrl = URL(string: weatherUrlString)
guard weatherUrl != nil else {
return
}
let request = URLRequest(url: weatherUrl!)
//Create a URL session
let session = URLSession.shared
let dataTask = session.dataTask(with: request) { data, response, error in
guard error == nil else {
return
}
do{
let decoder = JSONDecoder()
var result = try decoder.decode(Weather.self, from: data!)
//parsing the weather data into the constant, result
//Add UUId's to the hourly weather objects. Use the variable Result since that is parsing the weather
for i in 0..<result.hourly.count {
result.hourly[i].id = UUID()
}
DispatchQueue.main.async {
self.weatherData = result
}
}catch {
print(error)
}
}
dataTask.resume()
}
}
}//func getWeatherData
}
My Model
struct Weather: Decodable {
var current: Current
var hourly: [Current]
//Hourly is an arrary of weather responses (i.e. Current). It parses the data because the arrary is similar to Current properties
}
struct Current: Decodable, Identifiable {
var id: UUID?
var dt: Double
var temp: Double
var feels_like: Double
var weather: [WeatherInfo]
}
struct WeatherInfo: Decodable {
var description: String
}
Right now this is just a rough view and will update the look of it but for now I’m putting it as a list. I only want 12 hours of the hourly temperature rather than the 48 hours
View
List(model.weatherData?.hourly ?? [] ) {
hour in
Text("\(Constants.dtConversion(hour.dt)), \(Constants.tempToString(hour.temp))")
Related
When fetching data from the iTunes API https://itunes.apple.com/search?term=\(search)&entity=software&limit=14, the fetch fails if the limit is a larger number (e.g. 30, 40, 50 etc.). The limit is denoted by limit=14 found at the end of the URL. 14 is the number of results returned. This can be changed to any number.
When making the call in Postman, I can enter the limit as any number and it works without error. Additionally, when running the api with a large number in XCtest, the test passes. It only seems to fail when making the call live in the app.
The failure occurs in the guard let statement. In the code below, if the number is too large (e.g. 50), it prints "failed to fetch data" - indicating that there is a URL issue. When using a smaller number (e.g. 10), the fetch is successful and data returns in my table view. You can also change the search term. Currently I have it set to "Apple".
Below is the code for the API:
import Foundation
import UIKit
struct Response: Codable {
var resultCount: Int
var results: [Result]
}
struct Result: Codable {
var screenshotUrls, ipadScreenshotUrls, appletvScreenshotUrls: [String]
var artworkUrl60, artworkUrl512, artworkUrl100, artistViewUrl: String
var supportedDevices, advisories: [String]
var isGameCenterEnabled: Bool
var features: [String]
var kind, minimumOsVersion, trackCensoredName, fileSizeBytes: String
var contentAdvisoryRating: String
var genreIds: [String]
var primaryGenreName, artistName, trackContentRating, trackName, releaseDate, sellerName, currentVersionReleaseDate, releaseNotes, version: String
var primaryGenreId: Int
var currency, description: String
var price: Double
var averageUserRating: Double
}
class API {
var storedData = Response(resultCount: Int.init(), results: [])
func loadData(search: String, completionHandler: #escaping (Response) -> Void) {
guard let url = URL(string:"https://itunes.apple.com/search?term=\(search)&entity=software&limit=40") else {
print("failed to fetch data")
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(Response.self, from: data) {
DispatchQueue.main.async {
self.storedData.resultCount = response.resultCount
self.storedData.results = response.results
completionHandler(self.storedData)
}
return
}
}
print("failed \(error?.localizedDescription ?? "unknown error")")
}
.resume()
}
func reloadTableData() {
DataManager.shared.viewController.tableView.reloadData()
}
}
Any thoughts as to why a larger number causes the guard let to fail when running the app, but does not fail in my tests or postman?
EDIT
Below is how I am calling the function. I am calling it in viewdidload. It uses a completion handler, so it looks like the following:
api.loadData(search: "ibm") { Results in
self.filteredResults = self.api.storedData.results //stores value in filtered results array
self.tableView.reloadData() //refreshes table view - table view is referencing the filteredResults array
}
There are many keys missing in the JSON, when you set a limit of more than 20:
Always use doCatch when decoding JSON and print the error, which tells you what went wrong, in your source decoding is failing because of the following error: refer
keyNotFound(CodingKeys(stringValue: "releaseNotes", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "results", intValue: nil), _JSONKey(stringValue: "Index 15", intValue: 15)], debugDescription: "No value associated with key CodingKeys(stringValue: \"releaseNotes\", intValue: nil) (\"releaseNotes\").", underlyingError: nil))
Try making all variables in the struct Optional to fix decoding issue for now.
Make properties optional
struct Response: Codable {
var resultCount: Int?
var results: [Result]?
}
struct Result: Codable {
var screenshotUrls, ipadScreenshotUrls, appletvScreenshotUrls: [String]?
var artworkUrl60, artworkUrl512, artworkUrl100, artistViewUrl: String?
var supportedDevices, advisories: [String]?
var isGameCenterEnabled: Bool?
var features: [String]?
var kind, minimumOsVersion, trackCensoredName, fileSizeBytes: String?
var contentAdvisoryRating: String?
var genreIds: [String]?
var primaryGenreName, artistName, trackContentRating, trackName, releaseDate, sellerName, currentVersionReleaseDate, releaseNotes, version: String?
var primaryGenreId: Int?
var currency, description: String?
var price: Double?
var averageUserRating: Double?
}
Put this inside the if-let data block:
do {
let response = try JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
self.storedData.resultCount = response.resultCount
self.storedData.results = response.results
completionHandler(self.storedData)
}
} catch let error {
print(error)
}
I am learning Swift to develop macOS applications and I ran into a problem. I am trying to get certain data from a JSON from the internet. I have managed to get such data and put it in simple text labels in the view but when I run Xcode and get the values, if the values from the JSON get updated, I can't see it reflected in my app. I know that I must perform a function to refresh the data but what I have always found is the function to refresh the data that is in a table, not a simple text label.
Regardless of this problem, if I wanted to add a table with 3 columns (each structure has 3 data, at least) with the values from the JSON. When I run the refresh of the table, I should include in the function the function that gets the data from the internet, right? I'm a bit lost with this too.
This is what I have:
ViewController.swift
import Cocoa
class ViewController: NSViewController, NSTextFieldDelegate {
let user_items = UserItems()
#IBOutlet var item_salida: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
let struc_item = user_items.Item_Struct()
let position = struc_item.firstIndex(where: { $0.name == "Leanne Graham" })!
print(struc_item[position].state!)
item_salida.stringValue = struc_item[position].state!
} }
Struct.swift
import Foundation
import SwiftyJSON
// MARK: - Dato
struct User: Codable {
var name: String?
var username: String?
var email: String?
}
typealias Datos = [User]
class UserItems {
func Item_Struct() -> Datos {
let urlString = "https://jsonplaceholder.typicode.com/users"
var items_available: [User] = []
if let url = NSURL(string: urlString){
if let data = try? NSData(contentsOf: url as URL, options: []){
let items = try! JSONDecoder().decode([User].self, from: data as Data)
for item in items {
items_available.append(item)
}
}
}
return items_available
}
}
Thanks, a lot!
First of all - as you are learning Swift - please stop using snake_case variable names and also the NS.. classes NSURL and NSData.
Never load data from a remote URL with synchronous Data(contentsOf. It blocks the thread.
You need URLSession and an asynchronous completion handler.
// MARK: - Dato
struct User: Codable {
let name: String
let username: String
let email: String
}
typealias Datos = [User]
class UserItems {
func loadData(completion: #escaping (Datos) -> Void) {
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error { print(error); return }
do {
let items = try JSONDecoder().decode([User].self, from: data!)
completion(items)
} catch {
print(error)
}
}.resume()
}
}
And use it in the controller
class ViewController: NSViewController, NSTextFieldDelegate {
#IBOutlet var itemSalida: NSTextField!
let userItems = UserItems()
override func viewDidLoad() {
super.viewDidLoad()
userItems.loadData { users in
if let position = users.firstIndex(where: { $0.name == "Leanne Graham" }) {
DispatchQueue.main.async {
print(users[position].username)
self.itemSalida.stringValue = users[position].username
}
}
}
}
}
And forget SwiftyJSON. It's not needed anymore in favor of Codable.
I wanted to grab data from exchangeratesapi.io, but I have been struggling with modeling my data.
it says "Cannot assign value of type 'rates' to type rates.Type"
I have no idea what I did nor have any visualization , if there's any reference please do comment below.
Here's my class
class MoneyView:ObservableObject {
#Published var currency = rates.self//[rates]()
init() {
fetchData()
}
func fetchData() {
guard let url = URL(string: "http://api.exchangeratesapi.io/v1/latest?access_key=24a5ab7688a7044f60bfeb491eb37550") else {
return
}
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 result = try decoder.decode(rates.self, from: safeData)
DispatchQueue.main.async {
self.currency = result // here's the error
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
Here's the rates type :
struct rates: Decodable{
// USD CAD IDR GBP CHF SGD INR MYR JPY KRW
var USD:Int
var CAD:Int
var IDR:Int
var GBP:Int
var CHF:Int
var SGD:Int
var INR:Int
var MYR:Int
var JPY:Int
var KWR:Int
}
in case you guys wonder how the API looks like
{
"success":true,
"timestamp":1620597364,
"base":"EUR",
"date":"2021-05-09",
"rates":{
"AED":4.469059,
"AFN":93.55172,
"ALL":122.991702,
"AMD":629.683505,
"ANG":2.167635,
"AOA":795.883245,
}
}
Change
#Published var currency:Rate?
struct Root: Decodable{
var rates:Rate
}
struct Rate: Decodable{
var USD:Int
var CAD:Int
var IDR:Int
var GBP:Int
var CHF:Int
var SGD:Int
var INR:Int
var MYR:Int
var JPY:Int
var KWR:Int
}
let result = try decoder.decode(Root.self, from: safeData)
currency = result.rates
Change class start letter to capital Rates
I am trying to fetch the Covid 19 data of all countries and their states from disease.sh.
I have previously fetched json data from different APIs using this method. The response in those cases were shorter compared to this.
I have posted the codes below:
// Webservice.swift
import Foundation
class Webservice {
let countriesURL: String = "https://disease.sh/v3/covid-19/jhucsse"
func getAllCountries(completion: #escaping ([Country]?) ->()) {
guard let url = URL(string: countriesURL) else {
completion(nil)
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("No data in response: \(error?.localizedDescription ?? "Unknown error").")
DispatchQueue.main.async {
completion(nil)
}
return
}
let countries = try? JSONDecoder().decode([Country].self, from: data)
DispatchQueue.main.async {
countries == nil ? completion(nil) : completion(countries)
}
}.resume()
}
}
Since, I was using MVVM design, here are my Model, ViewModel and View.
// Model
// Country.swift
import Foundation
struct Country: Decodable {
var country: String
var updatedAt: String
var stats: Stats
var coordinates: Coordinates
var province: String
}
struct Stats: Decodable {
var confirmed: Int
var deaths: Int
var recovered: Int
}
struct Coordinates: Decodable {
var latitude: String
var longitude: String
}
// ViewModel
// CountryListViewModel.swift
import Foundation
class CountryListViewModel: ObservableObject {
#Published var countries = [CountryViewModel]()
init() {
fetchCountries()
}
func fetchCountries() {
Webservice().getAllCountries() { countries in
if let countries = countries {
self.countries = countries.map(CountryViewModel.init)
}
}
}
}
class CountryViewModel {
var country: Country
init(country: Country) {
self.country = country
}
let id = UUID()
var name: String {
return self.country.country
}
var updatedAt: String {
return self.country.updatedAt
}
var stats: Stats {
return self.country.stats
}
var coordinates: Coordinates {
return self.country.coordinates
}
var province: String {
return self.country.province
}
}
// View
// ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject private var countryListVM = CountryListViewModel()
var body: some View {
List( self.countryListVM.countries, id:\.id) { country in
Text(country.name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
My issue is when I call the Webservice.getAllCountries() it returns nil. Could anyone look at the code and tell me what is wrong, please? Thank you!
PS: I created a mock json with fewer objects (20-30) and called Webservice.getAllCountries() in this case it returned and mapped the values. It is not working with larger JSON response. Help!!
You should avoid using try ? except in situations where you really don't care about failures. do/try/catch is a better approach since it will tell you why something failed.
Changing your code to
do {
let countries = try JSONDecoder().decode([Country].self, from: data)
DispatchQueue.main.async {
completion(countries)
}
} catch {
print(error)
completion(nil)
}
Gives us an error on the console -
Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 509", intValue: 509), CodingKeys(stringValue: "province", intValue: nil)], debugDescription: "Expected String value but found null instead.", underlyingError: nil))
which makes sense, since not all countries have provinces. To fix this, make province an optional in your data model
struct Country: Decodable {
var country: String
var updatedAt: String
var stats: Stats
var coordinates: Coordinates
var province: String?
}
I'm making an api call and getting the response like so..
if let data = NSData(contentsOf: NSURL(string: "http://test.chatongo.in/testdata.json")! as URL) {
do {
if let response = try JSONSerialization.jsonObject(with: data as Data, options: []) as? NSDictionary {
print("THE RESPONSE IS: \(response)")
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
And the response I get like so...
THE RESPONSE IS: {
Message = Success;
Status = 200;
data = {
Records = (
{
Id = 1;
collectedValue = 500;
endDate = "10/06/2018";
mainImageURL = "http://iphonedeveloperguide.com/oneinr/project1.jpg";
shortDescription = "This foundation will bring smile on there faces";
startDate = "05/05/2018";
title = "Smile Crowdfunding";
totalValue = 5000;
},
{
Id = 2;
collectedValue = 750;
endDate = "08/06/2018";
mainImageURL = "http://iphonedeveloperguide.com/oneinr/project10.jpg";
shortDescription = "This foundation will help animals";
startDate = "05/05/2018";
title = "Animal Funding";
totalValue = 20000;
}
);
TotalRecords = 10;
};
}
But how do I parse this json and get the individual elements out of it including the image, that I'm not able to figure out.
You need
import UIKit
class ViewController: UIViewController {
var records = [Record]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
URLSession.shared.dataTask(with: URL(string: "http://test.chatongo.in/testdata.json")!) { (data, response, error) in
guard let data = data else { return }
do {
let res = try JSONDecoder().decode(Root.self, from: data)
self.records = res.data.records
print(res.data.records)
// if it's a collection/table wrap the reload here inside DispatchQueue.main.async
}
catch {
print(error)
}
}.resume()
}
}
// MARK: - Empty
struct Root: Codable {
let status: Int
let message: String
let data: DataClass
enum CodingKeys: String, CodingKey {
case status = "Status"
case message = "Message"
case data
}
}
// MARK: - DataClass
struct DataClass: Codable {
let totalRecords: Int
let records: [Record]
enum CodingKeys: String, CodingKey {
case totalRecords = "TotalRecords"
case records = "Records"
}
}
// MARK: - Record
struct Record: Codable {
let id: Int
let title, shortDescription: String
let collectedValue, totalValue: Int
let startDate, endDate: String
let mainImageURL: String
enum CodingKeys: String, CodingKey {
case id = "Id"
case title, shortDescription, collectedValue, totalValue, startDate, endDate, mainImageURL
}
}
Tip : Don't use NS stuff in swift and avoid using Data(contentsOf: as it blocks the main thread
There are multiple ways, one way to create a modelobject of
struct RecordItem : Codable {
var id : Int?
var collectedValue : Int?
var endDate : String?
var mainImageURL: String?
var shortDescription : String?
var title :String?
var startDate : String?
var totalValue : Int?
}
and
struct Records : Codable {
var items : [RecordItem]?
}
and use this. = let item = data [index]
print (item. collectedValue) and so on.
seconds methods, you already created dict, then extract all keys and array objects using ["Key": "Value"] and set to any variable.