SwiftUI JSON Decoder Response is 0 for APIResponse - json

I'm trying to fetch urls from an API based on a search query. If I hardcode the query parameter to some value in the url (i.e. "fitness"), I get a response.
If I set the query parameter to an interpolated value to be inserted at a later date, the app has no images at runtime-- which makes sense.
However, when I enter a search query into my search bar, I cannot fetch the results, either. In fact, my results are 0.
Here's the error:
po jsonResult
▿ APIResponse
- total : 0
- results : 0 elements
Here's my code:
Models
import Foundation
struct APIResponse: Codable {
let total: Int
let results: [Result]
}
struct Result: Codable {
let id: String
let urls: URLS
}
struct URLS: Codable {
let full: String
}
View
import SwiftUI
struct SimpleView: View {
#ObservedObject var simpleViewModel = SimpleViewModel.shared
#State private var searchText = ""
#State private var selected: String? = nil
var filteredResults: [Result] {
if searchText.isEmpty {
return simpleViewModel.results
} else {
return simpleViewModel.results.filter { $0.urls.full.contains(searchText) }
}
}
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 0) {
ForEach(filteredResults, id: \.id) { result in
NavigationLink(destination: SimpleDetailView()) {
VStack {
AsyncImage(url: URL(string: result.urls.full)) { image in
image.resizable()
} placeholder: {
ProgressView()
}
.scaledToFill()
.frame(maxWidth: .infinity)
.onTapGesture {
withAnimation(.spring()) {
if self.selected == result.urls.full {
self.selected = nil
} else {
self.selected = result.urls.full
}
}
hideKeyboard()
}
.scaleEffect(self.selected == result.urls.full ? 3.0 : 1.0)
}
}
}
}
}
.onAppear {
simpleViewModel.fetchPhotos(query: searchText)
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
}
}
}
struct SimpleView_Previews: PreviewProvider {
static var previews: some View {
SimpleView()
}
}
ViewModel
import Foundation
class SimpleViewModel: ObservableObject {
static let shared = SimpleViewModel()
private init() {}
#Published var results = [Result]()
func fetchPhotos(query: String) {
let url = "https://api.unsplash.com/search/photos?page=1&query=\(query)&client_id=blahblahblahblahblahblahblahblah"
guard let url = URL(string: url) else { return }
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let data = data, error == nil else { return }
do {
let jsonResult = try JSONDecoder().decode(APIResponse.self, from: data)
DispatchQueue.main.async {
self?.results = jsonResult.results
}
} catch {
print("Error: \(error)")
}
}
task.resume()
}
}
How can I search for images in my SimpleView based on my search query in my SimpleViewModel?
Setting breakpoints (how I discovered 0 values)
Ternary operators to check for search values or not
Setting my computer on fire
UPDATE
I added this to the code as #workingdog suggested, but with an else statement.
.onSubmit(of: .search) {
if searchText.isEmpty {
simpleViewModel.results = filteredResults
} else if !searchText.isEmpty {
simpleViewModel.fetchPhotos(query: searchText)
}
}
Here's what happens:
Images are fetched, but not displayed in view
Search query on submit renders nothing
Pressing cancel enacts the query
The images are displayed
Images remain and are displayed. Go back to 2.

In your SimpleView, the .onAppear { simpleViewModel.fetchPhotos(query: searchText } is
called only when the view appears, and uses searchText = "". In other words you have an empty query. So remove the .onAppear{...}, it does nothing.
Add something like this, to fetch the photos when the searchText
is submitted.
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.onSubmit(of: .search) {
if !searchText.isEmpty {
simpleViewModel.fetchPhotos(query: searchText)
}
}

Related

Parsing different queries with one func using SWIFT

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]?
}

SwiftUI: Instance member 'enableFilter' cannot be used on type 'ContentView'; did you mean to use a value of this type instead?

I'm a beginner with SwiftUI and I wanted to create a dynamic list with the birthdays of people. I also wanted to integrate a filter that facilitates to find birthdays. But in the lines:
if enableFilter == true {
return json.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return json
}
I always get these errors:
Instance member 'enableFilter' cannot be used on type 'ContentView';
did you mean to use a value of this type instead?
and
Instance member 'filterDate' cannot be used on type 'ContentView'; did
you mean to use a value of this type instead?
I think I understand why the errors are present but I don't no how to fix it. I tried:
#State static var
but then I cannot change the values with my
filterView
Thank you for your help, here is the full source code:
import SwiftUI
struct person: Codable, Hashable, Identifiable {
var id: Int
var Birthday: Date
var BirthdayString: String
}
func filter(date: Date) -> String {
let DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: date)
let DateComponentsString: String = "\(DateComponents.day)/\(DateComponents.month)/\(DateComponents.year)"
return DateComponentsString
}
struct ContentView: View {
#State var people: [person] = {
guard let data = UserDefaults.standard.data(forKey: "people") else { return [] }
if let json = try? JSONDecoder().decode([person].self, from: data) {
if enableFilter == true {
return json.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return json
}
}
return []
}()
#State var filterDate: Date = Date()
#State var enableFilter: Bool = false
#State var showFilter: Bool = false
#State var newPersonDate: Date = Date()
var body: some View {
NavigationView {
VStack {
HStack {
DatePicker(selection: $newPersonDate, label: {Text("Birthday")}).padding()
Button(action: {didTapAddTask()}, label: {Text("Add")}).padding()
}
List {
ForEach($people) { $person in
Text("\(person.Birthday)")
}
}
}
.navigationTitle(Text("People's birthday"))
}
}
var filterView: some View {
VStack {
DatePicker(selection: $filterDate, label: {Text("Date")}).padding()
Toggle(isOn: $enableFilter, label: {Text("enable filter")}).padding()
}
}
func didTapAddTask() {
let id = people.reduce(0) { max($0, $1.id) } + 1
people.insert(person(id: id, Birthday: newPersonDate, BirthdayString: filter(date: newPersonDate)), at: 0)
newPersonDate = Date()
save()
}
func save() {
guard let data = try? JSONEncoder().encode(people) else { return }
UserDefaults.standard.set(data, forKey: "people")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can use another Computed property which will get the data based on some another property's value. i.e enableFilter.
#State var people: [person] = {
guard let data = UserDefaults.standard.data(forKey: "people") else { return [] }
if let json = try? JSONDecoder().decode([person].self, from: data) {
return json
}
return []
}()
var data : [person] {
if enableFilter {
return people.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return people
}
}
And use this property to get the relevant data:
var body: some View {
NavigationView {
VStack {
HStack {
DatePicker(selection: $newPersonDate, label: {Text("Birthday")}).padding()
Button(action: {didTapAddTask()}, label: {Text("Add")}).padding()
}
List {
ForEach(data) { person in // <<--- Here `data`
Text("\(person.Birthday)")
}
}
}
.navigationTitle(Text("People's birthday"))
}
}

Why is my JSON code in Swift not parsing?

Disclaimer: Very basic question below. I am trying to learn the basics of IOS development.
I'm currently trying to parse data from an API to a SwiftUI project and am not able to successfully do so.
The code goes as follows:
import SwiftUI
struct Poem: Codable {
let title, author: String
let lines: [String]
let linecount: String
}
class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
struct ContentView: View {
#ObservedObject var fetch = FetchPoem()
let joined = fetch.poem.lines.joined(separator: "\n")
var body: some View {
Text(fetch.poem.title)
.padding()
Text( \(joined) )
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The build currently fails. It's throwing me the following errors:
Initializer 'init(_:)' requires that 'Binding<Subject>' conform to 'StringProtocol'
Referencing subscript 'subscript(dynamicMember:)' requires wrapper 'ObservedObject<FetchPoem>.Wrapper'
Value of type 'FetchPoem' has no dynamic member 'poem' using key path from root type 'FetchPoem'
Moreover, I am attempting to append the array "Lines" into one main String variable "Joined". However, I am not sure this works... The error is "String interpolation can only appear inside a string literal". Would love some help if anyone knows...
Any ideas? All help is appreciated.
** Edited Code - Q2
import SwiftUI
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
class FetchPoem: ObservableObject {
#Published var poems = [Poem]()
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
struct ContentView: View {
#ObservedObject var fetch = FetchPoem()
var body: some View {
VStack {
if let poem = fetch.poems.first {
Button("Refresh") {getPoem}
Text("\(poem.author): \(poem.title)").bold()
Divider()
ScrollView {
VStack {
ForEach(poem.lines, id: \.self) {
Text($0)
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Most probably you wanted this (because poems is an array) - tested with Xcode 12.1 / iOS 14.1
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
// ...
struct ContentView: View {
#ObservedObject var fetch = FetchPoem()
var body: some View {
List {
ForEach(fetch.poems, id: \.self) {
Text("\($0.author): \($0.title)")
}
}
}
}
... , and next might be wrapping that into NavigationView/NavigationLink with shown each poem in desination view.
Of showing lines as follows:
var body: some View {
VStack {
if let poem = fetch.poems.first {
Text("\(poem.author): \(poem.title)").bold()
Divider()
ScrollView {
VStack {
ForEach(poem.lines, id: \.self) {
Text($0)
}
}
}
}
}
}

SwiftUI: Data in list appears sometimes and sometimes not

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
}

SwiftUI: Display JSON Data with Text() instead of List

I have got a problem with displaying JSON data on a SwiftUI View.
I tried several tutorials and read articles which are related to my problem, but nothing seems appropriate enough.
For example everyone displays JSON data from an API with a fancy list or pictures, but I only want to know how you can simply display one word on a view (without List{}).
I chose the PokeAPI to figure out how to display "Hi Charmander" with the Text() instruction.
Example of a list (and without ObservableObject and #Published)
I want to get rid of the List and use sth. Text(resultsVar[0].name).onAppear(perform: loadData) like instead
import SwiftUI
struct pokeRequest:Codable {
var results: [Result]
}
struct Result:Codable {
var name:String
}
struct ViewOne: View {
#State var resultsVar = [Result]()
var body: some View {
VStack{
//unfortunately this does not work:
//Text(resultsVar[0].name).onAppear(perform: loadData)
List(resultsVar, id: \.name) { item in
VStack(alignment: .leading) {
Text("Hi \(item.name)")
}
}
.onAppear(perform: loadData)
}
}
func loadData(){
guard let url = URL(string: "https://pokeapi.co/api/v2/pokemon?offset=3&limit=3") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(pokeRequest.self, from: data) {
DispatchQueue.main.async {
self.resultsVar = decodedResponse.results
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
struct ViewOne_Previews: PreviewProvider {
static var previews: some View {
ViewOne()
}
}
Second try with a different approach (without .onAppear())
In this approach I tried with class: Observable Object and #Published but I also didn't come to my wished UI-output.
import SwiftUI
struct pokeRequest2:Codable {
var results2: [pokeEach2]
}
struct pokeEach2:Codable {
var name2:String
}
class Webservice:ObservableObject {
#Published var pokeInfo: [pokeRequest2] = [pokeRequest2]()
func decodepokemon() {
let session = URLSession.shared
let url = URL(string: "https://pokeapi.co/api/v2/pokemon?offset=3&limit=3")!
let task = session.dataTask(with: url) { data, response, error in
if error != nil || data == nil {
print("Client error!")
return
}
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
print("Server error!")
return
}
guard let mime = response.mimeType, mime == "application/json" else {
print("Wrong MIME type!")
return
}
do {
let response = try JSONDecoder().decode(pokeRequest2.self, from: data!)
print(self.pokeInfo[0].results2[0].name2)
DispatchQueue.main.async {
self.pokeInfo = [response]
}
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
init() {
decodepokemon()
}
}
struct ViewTwo: View {
#ObservedObject var webservice: Webservice = Webservice()
var body: some View {
Text("please help")
//Does also not work: Text(self.webservice.pokeInfo2[0].results2[0].name2)//.onAppear()
//Since a few minutes somehow the debug area prints "JSON error: The data couldn’t be read because it is missing." instead of "charmander"
}
}
struct ViewTwo_Previews: PreviewProvider {
static var previews: some View {
ViewTwo()
}
}
I tried several tutorials and read articles which are related to my problem, but nothing seems appropriate enough.
I would highly appreciate any help :-)
Thanks in advance!
I may be misunderstanding the question but in SwiftUI text can be displayed as follows:
Text("Hi" + item.name)
But as I say I'm not sure if that's the question.
As you will dynamically change the list items, you have to use .id() modifier. And in order to use .id(), the result must conforms to Hashable. The following code can help you solve the problem.
struct Result:Codable, Hashable {
var name:String
}
struct ViewOne: View {
#State var resultsVar = [Result]() // [Result(name: "firsy"),Result(name: "second"), ]//[Result]()
var body: some View {
VStack{
Spacer()
//unfortunately this does not work:
//Text(resultsVar[0].name).onAppear(perform: loadData)
List(resultsVar, id: \.name) { item in
VStack(alignment: .leading) {
Text("Hi \(item.name)")
}
}.id(resultsVar)
}.onAppear(perform: loadData)
I am by no means an expert (started using Swift less than three days ago) but I think I might have what you are looking for. This enables you to call a simple REST API that contains only a simple dictionary (in my case the dictionary contains "Differential": 5.22) and then display that number as text when a button is pressed. Hope the following is helpful!
struct DifferentialData: Decodable {
var Differential: Double
}
struct ContentView: View {
#State var data = DifferentialData(Differential: 0.00)
func getData() {
guard let url = URL(string: "Enter your URL here")
else { return } //Invalid URL
URLSession.shared.dataTask(with: url) { data, response, error in
let differentials = try! JSONDecoder().decode(DifferentialData.self, from: data!)
print(differentials)
DispatchQueue.main.async {
self.data = differentials
}
}
.resume()
}
var body: some View {
VStack{
Text("\(data.Differential)")
Button("Refresh data") {self.getData()}
}
}
}