WebImage not updating the View - SwiftUI (Xcode 12) - ios

My problem is that when I change my observed objects string property to another value, it updates the JSON Image values printing out the updated values(Using the Unsplash API), but the WebImage (from SDWebImageSwiftUI) doesn't change.
The struct that Result applies to:
struct Results : Codable {
var total : Int
var results : [Result]
}
struct Result : Codable {
var id : String
var description : String?
var urls : URLs
}
struct URLs : Codable {
var small : String
}
Here is the view which includes the webImage thats supposed to update:
struct MoodboardView: View {
#ObservedObject var searchObjectController = SearchObjectController.shared
var body: some View {
List {
VStack {
Text("Mood Board: \(searchObjectController.searchText)")
.fontWeight(.bold)
.padding(6)
ForEach(searchObjectController.results, id: \.id, content: { result in
Text(result.description ?? "Empty")
WebImage(url: URL(string: result.urls.small) )
.resizable()
.frame(height:300)
})
}.onAppear() {
searchObjectController.search()
}
}
}
}
Here is the class which does the API Request:
class SearchObjectController : ObservableObject {
static let shared = SearchObjectController()
private init() {}
var token = "gQR-YsX0OpwkYpbjhPVi3b4kSR-DtWrR5phwDm2kPMM"
#Published var results = [Result]()
#Published var searchText : String = "forest"
func search () {
let url = URL(string: "https://api.unsplash.com/search/photos?query=\(searchText)")
var request = URLRequest(url: url!)
request.httpMethod = "GET"
request.setValue("Client-ID \(token)", forHTTPHeaderField: "Authorization")
print("request: \(request)")
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {return}
print(String(data: data, encoding: .utf8)!)
do {
let res = try JSONDecoder().decode(Results.self, from: data)
DispatchQueue.main.async {
self.results.append(contentsOf: res.results)
}
//print(self.results)
} catch {
print("catch: \(error)")
}
}
task.resume()
}
}
Here is how I change the value of the searchText in a Button, if you would like to see:
struct GenerateView: View {
#ObservedObject var searchObjectController = SearchObjectController.shared
#State private var celsius: Double = 0
var body: some View {
ZStack {
Color.purple
.ignoresSafeArea()
VStack{
Text("Generate a Random Idea")
.padding()
.foregroundColor(.white)
.font(.largeTitle)
.frame(maxWidth: .infinity, alignment: .center)
Image("placeholder")
Slider(value: $celsius, in: -100...100)
.padding()
Button("Generate") {
print("topic changed to\(searchObjectController.searchText)")
searchObjectController.searchText.self = "tables"
}
Spacer()
}
}
}
}

It turns out you update the search text but don't search again. See this does the trick:
Button("Generate") {
print("topic changed to\(searchObjectController.searchText)")
searchObjectController.searchText.self = "tables" // you changed the search text but didnt search again
self.searchObjectController.search() // adding this does the trick
}
Also. I updated your code to use an EnvironmentObject. If you use one instance of an object throughout your app. Consider making it an EnvironmentObject to not have to pass it around all the time.
Adding it is easy. Add it to your #main Scene
import SwiftUI
#main
struct StackoverflowApp: App {
#ObservedObject var searchObjectController = SearchObjectController()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(self.searchObjectController)
}
}
}
And using it even simpler:
struct MoodboardView: View {
// Env Obj. so we reference only one object
#EnvironmentObject var searchObjectController: SearchObjectController
var body: some View {
Text("")
}
}
Here is your code with the changes and working as expected:
I added comments to the changes I made
struct ContentView: View {
var body: some View {
MoodboardView()
}
}
struct GenerateView: View {
#EnvironmentObject var searchObjectController: SearchObjectController
#State private var celsius: Double = 0
var body: some View {
ZStack {
Color.purple
.ignoresSafeArea()
VStack{
Text("Generate a Random Idea")
.padding()
.foregroundColor(.white)
.font(.largeTitle)
.frame(maxWidth: .infinity, alignment: .center)
Image("placeholder")
Slider(value: $celsius, in: -100...100)
.padding()
Button("Generate") {
print("topic changed to\(searchObjectController.searchText)")
searchObjectController.searchText.self = "tables" // you changed the search text but didnt search again
self.searchObjectController.search() // adding this does the trick
}
Spacer()
}
}
}
}
class SearchObjectController : ObservableObject {
//static let shared = SearchObjectController() // Delete this. We want one Object of this class in the entire app.
//private init() {} // Delete this. Empty Init is not needed
var token = "gQR-YsX0OpwkYpbjhPVi3b4kSR-DtWrR5phwDm2kPMM"
#Published var results = [Result]()
#Published var searchText : String = "forest"
func search () {
let url = URL(string: "https://api.unsplash.com/search/photos?query=\(searchText)")
var request = URLRequest(url: url!)
request.httpMethod = "GET"
request.setValue("Client-ID \(token)", forHTTPHeaderField: "Authorization")
print("request: \(request)")
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {return}
print(String(data: data, encoding: .utf8)!)
do {
let res = try JSONDecoder().decode(Results.self, from: data)
DispatchQueue.main.async {
self.results.append(contentsOf: res.results)
}
//print(self.results)
} catch {
print("catch: \(error)")
}
}
task.resume()
}
}
struct MoodboardView: View {
// Env Obj. so we reference only one object
#EnvironmentObject var searchObjectController: SearchObjectController
var body: some View {
List {
VStack {
Text("Mood Board: \(searchObjectController.searchText)")
.fontWeight(.bold)
.padding(6)
ForEach(searchObjectController.results, id: \.id, content: { result in
Text(result.description ?? "Empty")
WebImage(url: URL(string: result.urls.small) )
.resizable()
.frame(height:300)
})
}.onAppear() {
searchObjectController.search()
}
// I added your update button here so I can use it.
GenerateView()
}
}
}
struct Results : Codable {
var total : Int
var results : [Result]
}
struct Result : Codable {
var id : String
var description : String?
var urls : URLs
}
struct URLs : Codable {
var small : String
}
Ps. please not that it appears when you search you just append the results to the array and don't delete the old ones. Thats the reason why you still see the first images after updating. The new ones just get appended at the bottom. Scroll down to see them. If you don't want that just empty the array with results upon search

Related

Show API Data in a View without ID

I have a small weather project and I got stuck in the phase where I have to show the results from the API in a view. The API is from WeatherAPI.
I mention that the JSON file doesn't have an id and I receive the results in Console.
What is the best approach for me to solve this problem?
Thank you for your help!
This is the APIService.
import Foundation
class APIService: ObservableObject {
func apiCall(searchedCity: String) {
let apikey = "secret"
guard let url = URL(string: "https://api.weatherapi.com/v1/current.json?key=\(apikey)&q=\(searchedCity)&aqi=no") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-type")
let task = URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data, error == nil else {
return
}
do {
let response = try JSONDecoder().decode(WeatherModel.self, from: data)
print(response)
print("SUCCESS")
}
catch {
print(error)
}
}
task.resume()
}
}
This is the ForecastModel
import Foundation
struct WeatherModel: Codable {
var location: Location
var current: Current
}
struct Location: Codable {
var name: String
}
struct Current: Codable, {
var temp_c: Decimal
var wind_kph: Decimal
}
Here is the view where I want to call.
import SwiftUI
struct WeatherView: View {
#StateObject var callApi = APIService()
#Binding var searchedCity: String
var body: some View {
NavigationView {
ZStack(alignment: .leading) {
Color.backgroundColor.ignoresSafeArea(edges: .all)
VStack(alignment: .leading, spacing: 50) {
comparation
temperature
clothes
Spacer()
}
.foregroundColor(.white)
.font(.largeTitle)
.onAppear(perform: {
self.callApi.apiCall(searchedCity: "\(searchedCity)")
})
}
}
}
}
struct WeatherView_Previews: PreviewProvider {
#State static var searchedCity: String = ""
static var previews: some View {
WeatherView(searchedCity: $searchedCity)
}
}
fileprivate extension WeatherView {
var comparation: some View {
Text("Today is ")
.fontWeight(.medium)
}
var temperature: some View {
Text("It is ?C with ? and ? winds")
.fontWeight(.medium)
}
var clothes: some View {
Text("Wear a ?")
.fontWeight(.medium)
}
}

Swift JSON Parsing issue - Possible problem with struct?

Upon login and validation, the user is sent to the main application page. I have the following code set.
import SwiftUI
typealias MyDefendant = [Defendant]
struct ContentView: View {
var email: String
#State var myDefendant: MyDefendant = []
func getUserData(completion:#escaping (MyDefendant)->()) {
var urlRequest = URLRequest(url: URL(string: "https://milanobailbonds.com/getDefendant.php")!)
urlRequest.httpMethod = "post"
let authData = [
"defEmail" : email
] as [String : Any]
do {
let authBody = try JSONSerialization.data(withJSONObject: authData, options: .prettyPrinted)
urlRequest.httpBody = authBody
urlRequest.addValue("application/json", forHTTPHeaderField: "content-type")
} catch let error {
debugPrint(error.localizedDescription)
}
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
return
}
guard let data = data else {
return
}
do {
let responseString = String(data: data, encoding: .utf8)!
print(responseString)
var returnValue: MyDefendant?
let decoder = JSONDecoder()
returnValue = try decoder.decode([Defendant].self, from: data)
completion(returnValue!)
}
catch { fatalError("Couldn't Parse")
}
}.resume()
return
}
var body: some View {
NavigationView {
VStack {
Text(email)
Text("I Need Bail")
.font(.largeTitle)
.fontWeight(.semibold)
Button {
print("Test")
} label: {
Label("I Need Bail", systemImage: "iphone.homebutton.radiowaves.left.and.right")
.labelStyle(IconOnlyLabelStyle())
.font(.system(size: 142.0))
}
} .foregroundColor(.green)
.shadow(color: .black, radius: 2, x: 2, y: 2)
.navigationBarTitle("Home")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading:
Button {
print("Test")
} label: {
Label("I Need Bail", systemImage: "line.3.horizontal")
.labelStyle(IconOnlyLabelStyle())
})
}.onAppear() {
getUserData() { myDefendant in
self.myDefendant = myDefendant
}
}
}
}
In my data models I have created a struct for Defendant as such:
struct Defendant: Codable, Hashable, Identifiable {
var id: Int
var defImage: String
var defName: String
var defAddress: String
var defCity: String
var defState: String
var defZip: String
var defPhone: String
var defEmail: String
var defUserName: String
var defPW: String
var defDOB: String
var defPriorFTA: Int
var defFTAExplained: String
var defAssignedAgency: Int
} // Defendant Model
The PHP is working fine and returning valid JSON with all of the required items for the struct.
"\"[\\n {\\n \\\"Id\\\": 5,\\n \\\"defImage\\\": \\\"\\\",\\n \\\"defName\\\": \\\"Some Dude\\\",\\n \\\"defAddress\\\": \\\"123 Main St\\\",\\n \\\"defCity\\\": \\\"Some City\\\",\\n \\\"defState\\\": \\\"FL\\\",\\n \\\"defZip\\\": \\\"12345\\\",\\n \\\"defPhone\\\": \\\"888-888-8888\\\",\\n \\\"defEmail\\\": \\\"someone#someone.com\\\",\\n \\\"defUserName\\\": \\\"\\\",\\n \\\"defPW\\\": \\\"91492cffa4032765f6b025ec6b2c873e49fe5e58\\\",\\n \\\"defDOB\\\": \\\"01\\\\\\/01\\\\\\/1955\\\",\\n \\\"defPriorFTA\\\": 0,\\n \\\"defFTAExplained\\\": \\\"\\\",\\n \\\"defAssignedAgency\\\": 0\\n }\\n]\""
Unfortunately, I keep getting an error "Unable to Parse".
I'm new to Swift, and coding in general.
Any thoughts or ideas are greatly appreciated.
Thank you
Im not sure but you can try to change id into Id in your struct. Please remember name of your struct property must exactly the same with key in Json response.

It does not fetch data inside onAppear

I have api links that are inside the api link
I have this class and I fetched the data from it so that it gives me the API links
class Api : ObservableObject{
#Published var modelApiLink : [model] = []
func getDataModelApi () {
guard let url = URL(string: APIgetURL.demo) else { return }
var request = URLRequest(url: url)
let token = "38|Xxxxxxx"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
do {
let dataModel = try JSONDecoder().decode([model].self, from: data)
DispatchQueue.main.async {
self.modelApiLink = dataModel
}
} catch {
print("error: ", error)
}
}
.resume()
}
}
It's ready now
And I made another function to fetch the API data from the previous one
#Published var models : [model] = []
func getData (url : String) {
// Exactly the same as func last, but needs to put a link
}
And here I presented the data and its work in onAppear
But it does not give any data
struct VideoViewAll : View {
#StateObject var model = Api()
#StateObject var modelApiLink = Api()
‏ var body: some View {
‏ VStack {
‏ ScrollView(.vertical, showsIndicators: false) {
‏
‏
ForEach(model.models) { item in
‏ HStack {
‏ NavigationLink(destination: detiles(model: item)) {
‏ Spacer()
‏ VStack(alignment: .trailing, spacing: 15) {
Text(item.title)
‏ }.padding(.trailing, 5)
}
}
}
‏ .onAppear() {
‏ modelApiLink.getDataModelApi()
‏ for itemModel in modelApiLink.models {
‏ model.getData(url: itemModel.url)
}
}
}
}

Where to make an API call to populate a view with data

I am trying to create a view that displays results from an API call, however I keep on running into multiple errors.
My question is basically where is the best place to make such an API call.
Right now I am "trying" to load the data in the "init" method of the view like below.
struct LandingView: View {
#StateObject var viewRouter: ViewRouter
#State var user1: User
#State var products: [Product] = []
init(_ viewRouter : ViewRouter, user: User) {
self.user1 = user
_viewRouter = StateObject(wrappedValue: viewRouter)
ProductAPI().getAllProducts { productArr in
self.products = productArr
}
}
var body: some View {
tabViewUnique(prodArrParam: products)
}
}
I keep on getting an "escaping closure mutating self" error, and while I could reconfigure the code to stop the error,I am sure that there is a better way of doing what I want.
Thanks
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
}
// this one onAppear you can use it
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") 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([TaskEntry].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
}
In .onAppear you can make api calls

How to set up SwiftUI app to pass inputted value as parameter into URL string

I am attempting to build a basic SwiftUI weather app. The app allows the user to search weather by city name, using the OpenWeatherMap API. I configured the inputted city name from the text field to be injected into name: "" in WeatherModel, inside the fetchWeather() function in the viewModel. I then configured the OpenWeatherMap URL string to take in searchedCity.name as a parameter (see viewModel below). This setup seems to work fine, as I am able to search for weather by city name. However, I want to seek feedback as to whether or not the practice of passing searchCity.name directly into the URL (in the viewModel) is correct. In regards to:
let searchedCity = WeatherModel(...
... I am not sure what to do with the CurrentWeather and WeatherInfo inside that instance of WeatherModel. Since I'm only using "searchedCity" to pass the name of the city into the URL, how should "CurrentWeather.init(temp: 123.00)" and "weather: [WeatherInfo.init(description: "")]" be set? Is it correct to implement values for temp and description, such as 123 and ""?
Here is my full code below:
ContentView
import SwiftUI
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
// #State private var textField = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $viewModel.enterCityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather()
viewModel.enterCityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
Text(viewModel.title)
.font(.system(size: 32))
Text(viewModel.temp)
.font(.system(size: 44))
Text(viewModel.descriptionText)
.font(.system(size: 24))
Spacer()
}
.navigationTitle("Weather MVVM")
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Model
import Foundation
// Data, Model should mirror the JSON layout
//Codable is the property needed to convert JSON into a struct
struct WeatherModel: Codable {
let name: String
let main: CurrentWeather
let weather: [WeatherInfo]
}
struct CurrentWeather: Codable {
let temp: Float
}
struct WeatherInfo: Codable {
let description: String
}
ViewModel
import Foundation
class WeatherViewModel: ObservableObject {
//everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
#Published var title: String = "-"
#Published var temp: String = "-"
#Published var descriptionText: String = "-"
#Published var enterCityName: String = ""
init() {
fetchWeather()
}
func fetchWeather() {
let searchedCity = WeatherModel(name: enterCityName, main: CurrentWeather.init(temp: 123.00), weather: [WeatherInfo.init(description: "")])
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(searchedCity.name)&units=imperial&appid=<myAPIKey>") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// get data
guard let data = data, error == nil else {
return
}
//convert data to model
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.title = model.name
self.temp = "\(model.main.temp)"
self.descriptionText = model.weather.first?.description ?? "No Description"
}
}
catch {
print(error)
}
}
task.resume()
}
}
There are many ways to do what you ask, the following code is just one approach. Since you only need the city name to get the result, just use only that in the url string. Also using your WeatherModel in the WeatherViewModel avoids duplicating the data into various intermediate variables.
PS: do not post your secret appid key in your url.
import Foundation
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = "" // <-- use this to get the city name
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName) // <-- let the model fetch the results
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
// --- display the results ---
Text(viewModel.cityWeather.name).font(.system(size: 32))
Text("\(viewModel.cityWeather.main.temp)").font(.system(size: 44))
Text(viewModel.cityWeather.firstWeatherInfo()).font(.system(size: 24))
Spacer()
}
.navigationTitle("Weather MVVM")
}.navigationViewStyle(.stack)
}
}
class WeatherViewModel: ObservableObject {
// use your WeatherModel that you get from the fetch results
#Published var cityWeather: WeatherModel = WeatherModel()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityWeather = model
}
}
catch {
print(error) // <-- need to deal with errors here
}
}
task.resume()
}
}
struct WeatherModel: Codable {
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
I want to seek feedback as to whether or not the practice of passing searchCity.name directly into the URL (in the viewModel) is correct.e
You should alway avoid to pass fake values to an object/class like Int(123).
Instead you should use nullable structures or classes.
I don't see the need of create a whole WeatherModel instance just to read one property from it, one property that you already have in a viewmodel's enterCityName property. Just use the viewmodel's enterCityName property instead.

Resources