My problem is - I'm building Weather App that displays 20 different cities at the same time (that's the task). I can do it with one city when i put it in guard let url = URL(string: ) directly like this (London)
struct Constants {
static let API_KEY = "<api-key>"
static let baseURL = "https://api.openweathermap.org/data/2.5/weather?appid=\(API_KEY)&units=metric&q=" // + cityName
}
class APICaller {
static let shared = APICaller()
func getData(completion: #escaping(Result<[WeatherDataModel], Error>) -> Void) {
guard let url = URL(string: "\(Constants.baseURL)London") else { return } // Here is the city i've put
let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in
guard let data = data, error == nil else {
return
}
do {
let results = try JSONDecoder().decode(MainWeatherDataModel.self, from: data)
completion(.success(results.results))
} catch {
completion(.failure(error))
}
}
task.resume()
}
}
My project contains CollectionView inside TableView. Parsed data filling Cells
But it's only one city showing in App. I need 19 more.
So my questions are: How can I implement different queries in URL or Is there a method do to multiple parsing?
Thank you
Here is a very basic example code, to fetch the weather for a number of cities using your modified setup. It shows how to implement different queries using the URL, as per the question.
Note, you should read about (and use) Swift async/await concurrency, to fetch
all the data concurrently.
struct Constants {
static let API_KEY = "api-key"
static let baseURL = "https://api.openweathermap.org/data/2.5/weather?appid=\(API_KEY)&units=metric&q="
}
class APICaller {
static let shared = APICaller()
// -- here
func getData(cityName: String, completion: #escaping(Result<[WeatherDataModel], Error>) -> Void) {
// -- here
guard let url = URL(string: (Constants.baseURL + cityName)) else { return }
URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in
guard let data = data, error == nil else { return }
do {
let results = try JSONDecoder().decode(MainWeatherDataModel.self, from: data)
// -- here
if let weather = results.weather {
completion(.success(weather))
} else {
completion(.success([]))
}
} catch {
completion(.failure(error))
}
}.resume()
}
}
struct ContentView: View {
#State var citiesWeather: [String : [WeatherDataModel]] = [String : [WeatherDataModel]]()
#State var cities = ["London", "Tokyo", "Sydney"]
var body: some View {
List(cities, id: \.self) { city in
VStack {
Text(city).foregroundColor(.blue)
Text(citiesWeather[city]?.first?.description ?? "no data")
}
}
.onAppear {
for city in cities {
fetchWeatherFor(city) // <-- no concurrency, not good
}
}
}
func fetchWeatherFor(_ name: String) {
APICaller.shared.getData(cityName: name) { result in
switch result {
case .success(let arr): citiesWeather[name] = arr
case .failure(let error): print(error) // <-- todo
}
}
}
}
struct WeatherDataModel: Identifiable, Decodable {
public let id: Int
public let main, description, icon: String
}
struct MainWeatherDataModel: Identifiable, Decodable {
let id: Int
let weather: [WeatherDataModel]?
}
Related
I am trying to create a list of text objects out of a function that returns an array of params. Everything seems to be working fine, getting the data, console shows the correct results, except the list itself which remains empty.
The function call:
import UIKit
import SwiftUI
struct SubdomainsList: View {
#State var SubDomains = VTData().funky(XDOMAIN: "giphy.com")
var body: some View {
VStack {
List{
Text("Subdomains")
ForEach(SubDomains, id: \.self) { SuDo in
Text(SuDo)
}
}
}
}
}
struct SubdomainsList_Previews: PreviewProvider {
static var previews: some View {
SubdomainsList()
}
}
The Json handlers:
struct VTResponse: Decodable {
let data: [VT]
}
struct VT: Decodable {
var id: String
}
The class:
class VTData {
func funky (XDOMAIN: String) -> Array<String>{
var arr = [""]
getDATA(XDOMAIN: "\(XDOMAIN)", userCompletionHandler: { (SubDomain) in
print(SubDomain)
arr.append(SubDomain)
return SubDomain
})
return arr
}
func getDATA(XDOMAIN: String, userCompletionHandler: #escaping ((String) -> String)) {
let token = "<TOKEN>"
guard let url = URL(string: "https://www.lalalla.com/subdomains") else {fatalError("Invalid URL")}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("\(token)", forHTTPHeaderField: "x-apikey")
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
guard let data = data else { return }
let decoder = JSONDecoder()
let result = try? decoder.decode(VTResponse.self, from: data)
if let result = result {
for SubDo in result.data {
let SubDomain = SubDo.id
userCompletionHandler(SubDomain)
}
}
else {
fatalError("Could not decode")
}
})
task.resume()
}
}
I'm getting no errors whatsoever, and the console output shows the correct results:
support.giphy.com
cookies.giphy.com
media3.giphy.com
i.giphy.com
api.giphy.com
developers.giphy.com
media.giphy.com
x-qa.giphy.com
media2.giphy.com
media0.giphy.com
It is also worth mentioning that when I add print(type(of: SubDomain)) to the code I'm getting a String rather than an array.
The preview:
preview
Any ideas?
try this approach, again, to extract the list of subdomain from your API, and display them in
a List using the asynchronous function getDATA(...):
class VTData {
// `func funky` is completely useless, remove it
func getDATA(XDOMAIN: String, completion: #escaping ([String]) -> Void) { // <-- here
let token = "<TOKEN>"
guard let url = URL(string: "https://www.virustotal.com/api/v3/domains/\(XDOMAIN)/subdomains") else {
print("Invalid URL")
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("\(token)", forHTTPHeaderField: "x-apikey")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else { return } // todo return some error msg
do {
let results = try JSONDecoder().decode(VTResponse.self, from: data)
return completion(results.data.map{ $0.id }) // <-- here
} catch {
print(error) // <-- here important
}
}.resume()
}
}
struct SubdomainsList: View {
#State var subDomains: [String] = [] // <--- here
var body: some View {
VStack {
List{
Text("Subdomains")
ForEach(subDomains, id: \.self) { SuDo in
Text(SuDo)
}
}
}
.onAppear {
// async function
VTData().getDATA(XDOMAIN: "giphy.com") { subs in // <--- here
subDomains = subs
}
}
}
}
I am having issues trying to get this data. I heard there is a trick. Can anyone create a simple call to view the data from this api? Would truly appreciate it. Been trying for a week. I cant for the life of me get this simple api call to work.
http://api.citybik.es/v2/networks
Model.swift
import Foundation
// MARK: - Welcome
struct Dataset: Codable {
let networks: [Network]
}
// MARK: - Network
struct Network: Codable {
let company: [String]
let href, id: String
let location: Location
let name: String
}
// MARK: - Location
struct Location: Codable {
let city, country: String
let latitude, longitude: Double
}
Contentview.swift
import SwiftUI
struct ContentView: View {
#State var results = [Network]()
func loadData() {
guard let url = URL(string: "http://api.citybik.es/v2/networks") else {
print("Your API end point is Invalid")
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([Network].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
var body: some View {
List(results, id: \.name) { item in
VStack(alignment: .leading) {
Text("\(item.name)")
}
}.onAppear(perform: loadData)
}
}
copy the whole of the json from : "https://api.citybik.es/v2/networks"
into "https://app.quicktype.io/" and get the (correct) swift data structures from that.
Rename "Welcome" to "Response" and use that in your code.
use: "https://api.citybik.es/v2/networks" note the https.
EDIT: In your code:
struct ContentView: View {
#State var networks = [Network]()
var body: some View {
List(networks, id: \.id) { network in
VStack {
Text(network.name)
Text(network.location.city)
}
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.citybik.es/v2/networks") else {
print("Your API end point is Invalid")
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.networks = response.networks
}
return
}
}
}.resume()
}
}
Once you have all the data structures,
and if you are using Swift 5.5 for ios 15 or macos 12, you can use something like this:
struct ContentView: View {
#State var networks = [Network]()
var body: some View {
List(networks, id: \.id) { network in
VStack {
Text(network.name)
Text(network.location.city)
}
}
.task {
let response: Response? = await fetchNetworks()
if let resp = response {
networks = resp.networks
}
}
}
func fetchNetworks<T: Decodable>() async -> T? {
let url = URL(string: "https://api.citybik.es/v2/networks")!
let request = URLRequest(url: url)
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
// throw URLError(.badServerResponse) // todo
print(URLError(.badServerResponse))
return nil
}
let results = try JSONDecoder().decode(T.self, from: data)
return results
}
catch {
return nil
}
}
}
Beginner here, in a bit over my head with this. ;)
I've found examples that have shown me how to get data from a JSON API feed if the feed is structured as an array of objects, but I don't know how to approach getting the data (specifically, url and title) if the data I'm retrieving comes back in a more complex nested structure like this one:
{
"races": {
"videos": [{
"id": 1,
"url": "firsturl",
"title": "1st Video Title"
}, {
"id": 2,
"url": "secondurl",
"title": "2nd Video Title"
}]
}
}
I've succeeded at get data from another API feed that's structured as a simple array of objects--it's like what's above but without the extra two lead-in objects, namely this: { "races": { "videos":
Here's the code I pieced together from a few examples that worked for the simple array:
import SwiftUI
struct Video: Codable, Identifiable {
public var id: Int
public var url: String
public var title: String
}
class Videos: ObservableObject {
#Published var videos = [Video]()
init() {
let url = URL(string: "https://exampledomain.com/jsonapi")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let videoData = data {
let decodedData = try JSONDecoder().decode([Video].self, from: videoData)
DispatchQueue.main.async {
self.videos = decodedData
}
} else {
print("no data found")
}
} catch {
print("an error occurred")
}
}.resume()
}
}
struct VideosView: View {
#ObservedObject var fetch = Videos()
var body: some View {
VStack {
List(fetch.videos) { video in
VStack(alignment: .leading) {
Text(video.title)
Text("\(video.url)")
}
}
}
}
}
I've spent several hours over a few days reading and watching tutorials, but so far nothing is sinking in to help me tackle the more complex JSON API feed. Any tips would be greatly appreciated!
UPDATE:
With the help of a Swift Playground tutorial and the suggested structs mentioned in the comments below, I've succeeded at retrieving the more complex data, but only in Swift Playgrounds, using this:
import SwiftUI
struct Welcome: Codable {
let races: Races
}
struct Races: Codable {
let videos: [Video]
}
struct Video: Codable {
let id: Int
let url, title: String
}
func getJSON<T: Decodable>(urlString: String, completion: #escaping (T?) -> Void) {
guard let url = URL(string: urlString) else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
completion(nil)
return
}
guard let data = data else {
completion(nil)
return
}
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(T.self, from: data) else {
completion(nil)
return
}
completion(decodedData)
}.resume()
}
getJSON(urlString: "https://not-the-real-domain.123/api/") { (followers:Welcome?) in
if let followers = followers {
for result in followers.races.videos {
print(result.title )
}
}
}
Now, I'm struggling with how to properly integrate this Playgrounds snippet in to the working SwiftUI file's VideosViews, etc.
UPDATE 2:
import SwiftUI
struct Welcome: Codable {
let races: RaceItem
}
struct RaceItem: Codable {
let videos: [VideoItem]
}
struct VideoItem: Codable {
let id: Int
let url: String
let title: String
}
class Fetcher: ObservableObject {
func getJSON<T: Decodable>(urlString: String, completion: #escaping (T?) -> Void) {
guard let url = URL(string: urlString) else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
completion(nil)
return
}
guard let data = data else {
completion(nil)
return
}
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(T.self, from: data) else {
completion(nil)
return
}
completion(decodedData)
}.resume()
}
}
struct JSONRacesView: View {
#ObservedObject var fetch = Fetcher()
getJSON(urlString:"https://not-the-real-domain.123/api/") { (followers:Welcome?) in
if let followers = followers {
for result in followers.races.videos {
print(result.title )
}
}
}
var body: some View {
VStack {
List(fetch.tracks) { track in
VStack(alignment: .leading) {
Text(track.title)
Text("\(track.url)")
}
}
}
}
There's a great site called QuickType (app.quicktype.io) where you can paste in some JSON and get the Swift structs generated for you. Here's what it gives you:
import Foundation
// MARK: - Welcome
struct Welcome: Codable {
let races: Races
}
// MARK: - Races
struct Races: Codable {
let videos: [Video]
}
// MARK: - Video
struct Video: Codable {
let id: Int
let url, title: String
}
They have a bug in their template generator that mangles the demo line (I've submitted a pull request that is merged but isn't live on the site at the time of this writing), but here's what it should look like:
let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)
Using do/try so you can catch the errors, you can decode the data and reach the lower levels by doing:
do {
let welcome = try JSONDecoder().decode(Welcome.self, from: jsonData)
let videos = welcome.races.videos //<-- this ends up being your [Video] array
} catch {
//handle any errors
}
Update, based on your comments and updates:
You chose to go a little bit of a different route than my initial suggestion, but that's fine. The only thing that I would suggest is that you might want to deal with handling errors at some point rather than just returning nil in all of the completions (assuming you need to handle errors -- maybe it just not loading is acceptable).
Here's a light refactor of your code:
class Fetcher: ObservableObject {
#Published var tracks : [VideoItem] = []
private func getJSON<T: Decodable>(urlString: String, completion: #escaping (T?) -> Void) {
guard let url = URL(string: urlString) else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
completion(nil)
return
}
guard let data = data else {
completion(nil)
return
}
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(T.self, from: data) else {
completion(nil)
return
}
completion(decodedData)
}.resume()
}
func fetchData() {
getJSON(urlString:"https://not-the-real-domain.123/api/") { (followers:Welcome?) in
DispatchQueue.main.async {
self.tracks = followers?.races.videos ?? []
}
}
}
}
struct JSONRacesView: View {
#StateObject var fetch = Fetcher()
var body: some View {
VStack {
List(fetch.tracks, id: \.id) { track in
VStack(alignment: .leading) {
Text(track.title)
Text("\(track.url)")
}
}
}.onAppear {
fetch.fetchData()
}
}
}
You can see that now Fetcher has a #Published property that will store the tracks ([VideoItem]). getJSON is still in fetcher, but now it's private just to show that it isn't meant to be called directly. But, now there's a new function called fetchData() that your view will call. When fetchData gets data back, it sets the #Published property to that data. I used the ?? operator to tell the compiler that if followers is nil, then just use [] instead. This is all in a DispatchQueue.main.async block because the URL call is probably not going to return on the main thread and we need to make sure to always update the UI on the main thread (Xcode will warn you about this at runtime if you update the UI on a different thread).
JSONRacesView calls fetchData in onAppear, which happens exactly when it sounds like it will.
Last thing to note is I used #StateObject instead of #ObservedObject. If you're not on iOS 14 or macOS 11 yet, you could use #ObservedObject instead. There are some differences outside the scope of this answer, but that are easily Google-able.
I have a problem. In my application, I have CoreData with devices and the IP of the devices. I want to make an API request to fetch JSON data from a selected device and show them in a list. My problem is that sometimes it works and sometimes it does not and the list does not update when I change the data. I hope someone can help me.
BouquetAPIModel.swift
struct BouquetAPIModel: Codable {
let success: String
let data: DataClass
}
// MARK: - DataClass
struct DataClass: Codable {
let bouquets: [Bouquet]
}
// MARK: - Bouquet
struct Bouquet: Codable {
var number, name: String
}
Device.swift
public class Device: NSManagedObject, Identifiable {
#Published var bouquets : [Bouquet] = [Bouquet]()
func fetchBouquetList() {
fetchAPIRequest(apiPath: "/control/getbouquets?format=json") { (res: Result<BouquetAPIModel, Error>) in
switch res {
case .success(let bouquets):
self.bouquets = bouquets.data.bouquets
case .failure(let err):
print("Fail to fetch bouquets: ", err)
}
}
}
fileprivate func fetchAPIRequest<T: Decodable>(apiPath: String, completion: #escaping (Result<T, Error>) -> ()) {
let urlString = getApiUrl(apiPath: apiPath)
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data,resp, err) in
if let err = err {
completion(.failure(err))
return
}
do {
let welcome = try JSONDecoder().decode(T.self, from: data!)
completion(.success(welcome))
} catch let jsonError {
completion(.failure(jsonError))
}
}.resume()
}
BouquetView.swift
import SwiftUI
struct BouquetView: View {
#Binding var device : Device?
#State var bouquets: [Bouquet] = [Bouquet]()
var body: some View {
VStack {
Text(String(device?.deviceName ?? "fehler")).onTapGesture {
self.device?.bouquets.removeFirst()
print("Touch")
}
List (self.bouquets, id: \Bouquet.name) { bouquet in
VStack(alignment: .leading) {
Text(bouquet.name)
}
}
}.onAppear(perform: loadBouquets)
}
func loadBouquets() {
if device == nil {
//TODO jumo to settings
}
else {
device?.fetchBouquetList()
self.bouquets = device!.bouquets
}
}
}
struct BouquetView_Previews: PreviewProvider {
static var previews: some View {
BouquetView(device: .constant(nil))
}
}
Can you update your Device like below?
public class Device: ObservableObject, Identifiable {
...
}
And make sure you declare your device with annotation #ObservedObject before passing to BouquetView
Fetching might be long, so you've got into racing. To avoid it the followings approach might be used
1) remove the following line as data might not be ready yet at this point
self.bouquets = device!.bouquets
2) add explicit listener for data set
if device == nil {
//TODO jumo to settings
device!.$bouquets
.assign(to: \.bouquets, on: self)
.store(in: &cancellables) // << you will need to create this store var
}
I would like to download a picture of a json file online. This image should be displayed in a UIImageView.
At least I know that I need a JSON request but as I can bring the image in the ImageView I do not know
I've tried to download the image with a JSON request, but I have not managed it yet
My Code looks like this:
class HomeControllerTableView: UITableViewController{
var coursesPicture = [CoursePicture]()
var courseURL = [CourseURL]()
// MARK: - CoursePicture
struct CoursePicture: Codable {
let links: LinksPICTURE
enum CodingKeys: String, CodingKey {
case links = "_links"
}
}
// MARK: - Links
struct LinksPICTURE: Codable {
let wpFeaturedmedia: [WpFeaturedmedia]
enum CodingKeys: String, CodingKey {
case wpFeaturedmedia = "wp:featuredmedia"
}
}
// MARK: - WpFeaturedmedia
struct WpFeaturedmedia: Codable {
let href: String
}
struct CourseURL: Codable {
let guid: GUIDURL
}
// MARK: - GUID
struct GUIDURL: Codable {
let rendered: String
}
#IBOutlet weak var imageView: UIImageView!
var hello = ""
var hello2 = ""
override func viewDidLoad() {
super.viewDidLoad()
imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight, .flexibleBottomMargin, .flexibleTopMargin, .flexibleBottomMargin]
imageView.contentMode = .scaleAspectFill
tableView.isScrollEnabled = false
tableView.rowHeight = UITableView.automaticDimension
tableView.backgroundColor = .clear
self.tableView.backgroundView = imageView
tableView.addSubview(imageView)
fetchJSON()
fetchJSONPicture()
request()
fetchJSONURL()
}
public func fetchJSONPicture() {
let urlString = "EXAMPLE_URL"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, _, err) in
if let err = err {
print("Failed to get data from url:", err)
return
}
guard let data = data else { return }
do {
self.coursesPicture = [try JSONDecoder().decode(CoursePicture.self, from: data)]
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let jsonErr {
print("Failed to decode:", jsonErr)
}
}.resume()
}
public func fetchJSONURL() {
let urlString = hello
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, _, err) in
DispatchQueue.main.async {
if let err = err {
print("Failed to get data from url:", err)
return
}
guard let data = data else { return }
do {
self.courseURL = [try JSONDecoder().decode(CourseURL.self, from: data)]
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let jsonErr {
print("Failed to decode:", jsonErr)
}
}
}.resume()
if let url = URL(string: hello2) {
URLSession.shared.dataTask(with: url) { (data, urlResponse, error) in
if let data = data {
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
}
}.resume()
}
}
}
func request() {
let coursePicture = coursesPicture[0]// Index out of Range error
let courseUrl = courseURL[0] // Index out of Range error
hello = coursePicture.links.wpFeaturedmedia[0].href
hello2 = courseUrl.guid.rendered
}
}
The JSON Request should bring me an URL from the JSON File and store it in a global variable. After that i use the global variable to get the URL from the Image.
The Picture that i would fetch is in a WordPress Post as contributing picture.
I would like to make this code as dynamic as possible but the problem is that i became an error that is called "Index out of range".
Can me help someone maybe?
Thanks