SwiftUI: Why does this Boolean not change its value? - function

I try to change a binding bool in this function. Printing works as expected (so the transaction information is displayed correctly in the console), but the bool value - so the value of the var successfulPayment - isn't changing. I also tried to print the value of this bool after the print of "Payment was successful", but it was always false.
Note: the payment really is successful!
struct CheckView: View {
#Binding var successfulPayment: Bool
func getTransactionInformation () {
guard let url = URL(string: "URL-HERE") else {return}
URLSession.shared.dataTask(with: url){ [self] data, _, _ in
let transactioninformation = try! JSONDecoder().decode(IDValues.TransactionInformation.self, from: data!)
print(transactioninformation)
if (transactioninformation.id == transactionId && transactioninformation.statuscode == "Success") {
successfulPayment = true
print("Payment was successful!")
} else {
}
}
.resume()
}
}
I am pretty new to coding - what am I missing here?

Why do you put the session in an own struct: View which isn't really a view.
You can pass the binding to the func directly (wherever you prefer it to be). Here is an example:
struct ContentView: View {
#State private var successfulPayment: Bool = false
var body: some View {
Form {
Text("Payment is \(successfulPayment ? "successful" : "pending")")
Button("Pay") {
getTransactionInformation($successfulPayment)
}
}
}
func getTransactionInformation (_ success: Binding<Bool>) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
success.wrappedValue = true
}
}
}

Related

SwiftUI JSON Decoder Response is 0 for APIResponse

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)
}
}

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

Swift - Creating a stack (Text view) from a function that returns an array

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

SwiftUI: Waiting for JSON to be decoded before

I'm having an issue with displaying a deserialised JSON object in a view. The problem seems to be that my view is trying to unwrap a value from a published variable before anything is assigned to it by the function that gets the JSON object.
Here is the code that calls the api
class ViewModel : ObservableObject {
#Published var posts : first?
init(subReddit : String){
fetch(sub: subReddit)
}
func fetch(sub : String) {
guard let url = URL(string: "https://www.reddit.com/r/" + sub + "/top.json?t=day") else {
return
}
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let data = data, error == nil else {return}
do{
let retVal = try JSONDecoder().decode(first.self, from:data)
DispatchQueue.main.async {
self?.posts = retVal
}
}
catch{
print(error)
}
}
task.resume()
}
}
and here is the code for my view:
struct SubRedditView: View {
#StateObject var viewModel = ViewModel(subReddit: "all")
var body: some View {
NavigationView{
List{
ForEach((viewModel.posts?.data.children)!) {post in//at runtime I get a nil unwrap error here
Text(post.data.title)
Text(post.data.url_overridden_by_dest ?? "No Value")
}
}
.navigationTitle("Posts")
}
}
}
If only the object representing the children is relevant declare the published object as empty array of this type
#Published var posts = [TypeOfChildren]()
Then assign the children to the array
self?.posts = retVal.data.children
This makes the code in the view easier and safe.
ForEach(viewModel.posts) { post in
Text(post.title)
Text(post.url_overridden_by_dest ?? "No Value")

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
}