Display activity indicator while fetching api data - swift

I'm filling this Picker with data from my api
var body: some View {
NavigationView {
Form {
Section(header: Text("Pesquisar Denúncia")) {
//some code...
Picker(selection: $tipoDenunciaSelecionada, label: Text("Tipo de Denúncia")) {
ForEach(tiposDenuncia, id: \.self) {item in
Text(item.nome ?? "")
}
}.onAppear(perform: carregarTipoDenuncia)
//some more code...
}
}
For fetch data, i made this func
func carregarTipoDenuncia() {
self.estaCarregando = true
let oCodigo_Entidade = UserDefaults.standard.integer(forKey: "codigoEntidade")
guard let url = URL(string: "url here") else {
print("Erro ao conectar")
return
}
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) {data, response, error in
let decoder = JSONDecoder()
if let data = data {
do {
let response = try decoder.decode([TipoDenunciaModel].self, from: data)
DispatchQueue.main.async {
self.tiposDenuncia = response
}
return
} catch {
print(error)
}
}
}
task.resume()
}
but now i dont know to display an indicator that the data is being downloaded.
I tried adding a state boolean variable and manipulating it after response to show/hide an actionsheet but it didnt worked, the actionsheet didnt disapear.
I need the user to select at least one of the options of this picker.
How can i display some indicator that the data is loading?

As you can show any view in body add a boolean #State variable with initial value true and show a ProgressView. After loading the data set the variable to false which shows the Picker.
Move the .onAppear modifier to after Form.
var body: some View {
#State private var isLoading = true
// #State private var tipoDenunciaSelecionada ...
NavigationView {
Form {
Section(header: Text("Pesquisar Denúncia")) {
//some code...
if isLoading {
HStack(spacing: 15) {
ProgressView()
Text("Loading…")
}
} else {
Picker(selection: $tipoDenunciaSelecionada, label: Text("Tipo de Denúncia")) {
ForEach(tiposDenuncia, id: \.self) {item in
Text(item.nome ?? "")
}
}
//some more code...
}.onAppear(perform: carregarTipoDenuncia)
}
}
}
let response = try decoder.decode([TipoDenunciaModel].self, from: data)
DispatchQueue.main.async {
self.tiposDenuncia = response
self.isLoading = false
}
...

You can use a determinate SwiftUI ProgressView, as seen here.
Example usage:
import SwiftUI
struct ActivityIndicator: View {
#State private var someVar: CGFloat = 0.0 //You'll need some sort of progress value from your request
var body: some View {
ProgressView(value: someVar)
}
}

Related

How to call API again after change was made?

So I want to search books from google books api, but only through url query, how can I call API again when I enter the text in the search bar? How to reload the call?
I tried also with textfield onSumbit method, but nothing work.
I just want to insert value of textSearch to network.searchText and that network.searchText to insert into q=
here is my code of ContentView:
//
// ContentView.swift
// BookApi
//
// Created by Luka Šalipur on 7.6.22..
//
import SwiftUI
struct URLImage: View{
var urlString: String
#State var data: Data?
var body: some View{
if let data = data, let uiimage = UIImage(data:data) {
Image(uiImage: uiimage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:80, height:120)
.background(Color.gray)
} else {
Image(systemName: "book").onAppear {
fetch()
}
}
}
private func fetch(){
guard let url = URL(string: urlString) else {
return
}
let task = URLSession.shared.dataTask(with:url) { data, _, error in
self.data = data
}
task.resume()
}
}
// ContentView
struct ContentView: View {
#ObservedObject var network = Network()
#State var textSearch:String = "knjiga"
#State private var shouldReload: Bool = false
func context(){
network.searchText = self.textSearch
print(network.searchText)
}
var body: some View {
NavigationView{
List{
ForEach(network.book, id:\.self){ item in
NavigationLink{
Webview(url: URL(string: "\(item.volumeInfo.previewLink)")!)
} label: {
HStack{
URLImage(urlString: item.volumeInfo.imageLinks.thumbnail)
Text("\(item.volumeInfo.title)")
}
}
}
}
.onAppear{
context()
}
.onChange(of: textSearch, perform: { value in
self.shouldReload.toggle()
})
.searchable(text: $textSearch)
.navigationTitle("Books")
.task{
await network.loadData()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And here is my API(network) call:
//
// Network.swift
// BookApi
//
// Created by Luka Šalipur on 7.6.22..
//
import Foundation
import SwiftUI
class Network: ObservableObject{
#Published var book = [Items]()
var searchText: String = "watermelon" {
willSet(newValue) {
print(newValue)
}
}
func loadData() async {
guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=\(searchText)&key=API_KEY_PRIVATE") else {
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Books.self, from: data) {
book = decodedResponse.items
}
} catch {
print("There is an error")
}
}
}
This is a perfect candidate for the Combine framework.
In Network create a publisher which removes duplicates, debounces the input for 0.3 seconds, builds the URL, loads the data and decodes it.
I don't have your types, probably there are many errors. But this is a quite efficient way for dynamic searching. By the way your naming with regard to singular and plural form is pretty confusing.
import Combine
import SwiftUI
class Network: ObservableObject {
#Published var book = [Items]()
#Published var query = ""
private var subscriptions = Set<AnyCancellable>()
init() {
searchPublisher
.sink { completion in
print(completion) // show the error to the user
} receiveValue: { [weak.self] books in
self?.book = books.items
}
.store(in: &subscriptions)
}
var searchPublisher : AnyPublisher<Books,Error> {
return $query
.removeDuplicates()
.debounce(for: 0.3, scheduler: RunLoop.main)
.compactMap{ query -> URL? in
guard !query.isEmpty else { return nil }
guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=\(query)&key=API_KEY_PRIVATE") else {
return nil
}
return url
}
.flatMap { url -> AnyPublisher<Data, URLError> in
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.eraseToAnyPublisher()
}
.decode(type: Books.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
In the view create the view model (must be #StateObject!)
#StateObject var network = Network()
and bind searchable to query in network
.searchable(text: $network.query)
The view is updated when the data is available in network.book
The .task modifier ist not needed
There is another version of task that runs again when a value changes task(id:priority:_:). If a task is still running when the param changes it will be cancelled and restarted automatically. In your case use it as follows:
.task(id: textSearch) { newValue in
books = await getBooks(newValue)
}
Now we have async/await and task there is no need for an ObservableObject anymore.

Why is my function returning an empty array?

I am trying to call the results of this function in my SwiftUI view:
class GetMessages: ObservableObject {
let BASE_URL = "apicallurl.com"
#Published var messages = [Timestamp]()
func fetchMessages() {
guard let url = URL(string: BASE_URL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {print(error!.localizedDescription); return }
let theData = try! JSONDecoder().decode([String: Timestamp].self, from: data!)
DispatchQueue.main.async {
self.messages = Array(theData.values)
}
}
.resume()
}
}
I am testing the output with a print statement in the onAppear:
struct HomeTab: View {
#StateObject var getMsgs = GetMessages()
var body: some View {
NavigationView {
VStack(spacing: 0) {
greeting.edgesIgnoringSafeArea(.top)
messages
Spacer()
}
.onAppear {
print(getMsgs.fetchMessages())
print(getMsgs.messages)
}
}.navigationBarHidden(true)
}
both print statements print () or []
But when i print print(self.messages) in my GetMessages class the data prints fine.
Why is it empty in my Hometab view?
When you use getMsgs.fetchMessages() it may take some times to fetch the results. Once the results are available
the messages of getMsgs in HomeTab will be updated, and this will trigger a view refresh,
because it is a #StateObject and is "monitored" by the view.
However you should not try to print(getMsgs.messages) before the results are available.
So try the following sample code:
struct HomeTab: View {
#StateObject var getMsgs = GetMessages()
var body: some View {
NavigationView {
List {
ForEach(getMsgs.messages, id: \.self) { msg in
Text("\(msg)")
}
}
.onAppear {
getMsgs.fetchMessages()
// no printing of getMsgs.messages here
}
}.navigationBarHidden(true)
}
}

Find an item and change value in an array of structs

I'm trying to achieve the same as this post but instead of using class, I'm using a struct. I need to change loading to false whenever a criteria is met:
struct Media {
let id: String
let loading: Bool
let filePath: String
}
struct Attachedmedia: View {
//#State var loading: Bool
let loading: Bool
var body: some View {
ZStack {
Image("demo-x5")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100)
.border(Color.gray.opacity(0.3), width: 1)
.cornerRadius(8)
if loading {
ZStack {
Color.black.opacity(0.6).edgesIgnoringSafeArea(.all)
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
.cornerRadius(8)
}
}.frame(width: 100)
}
}
// This is the main view
struct MainView: View {
// save gets triggered but not shown here.
#State private var saved: Bool = false
#State private var media: [Media] = []
#State private var fileName: String = ""
var body: some View {
let threeColumnGrid = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
List {
Section {
LazyVGrid(columns: threeColumnGrid, spacing: 10) {
ForEach(media, id: \.id) {med in
// I need to be able to change the loading property
Attachedmedia(loading: med.loading)
}
}
}
}.onChange(of: saved, perform: { saved in
if saved {
let id = UUID().uuidString
let media = Media.init(id: id, loading: true, filePath: fileName)
self.media.append(media)
// Run in background
DispatchQueue.global(qos: .background).async {
Task {
do {
let response = try await doSomeThing()
// Switch back to main thread
DispatchQueue.main.async {
// When done, update loading to false
//self.media.filter({$0.id == id}).first?.loading = response.Error
if let row = self.media.firstIndex(where: {$0.id == id}) {
self.media[row] = Media.init(id: id, loading: false, filePath: fileName)
}
}
} catch {
print("Fail to upload: \(error)")
}
}
}
}
})
}
}
I'm uploading few images so the initial view will show a spinner. When finished uploading, I want to toggle the loading property to false but this bit of code not working:
[..]
// This
self.media.filter({$0.id == id}).first?.loading = response.Error
// Or this
if let row = self.media.firstIndex(where: {$0.id == id}) {
self.media[row] = Media.init(id: id, loading: false, filePath: fileName)
}
[..]
An example which shows the middle image not loading:
What do I need to do to be able to change the loading property to false so the spinner stop shows inside Attachedmedia?

SWIFTUI Observable Object Data Task only runs once?

I have an observable object class that downloads an image from a url to display:
class ImageLoader : ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
init(urlString:String){
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
print("imageloader1")
}
}
task.resume()
}
and I show it using:
struct ShowImage1: View {
#ObservedObject var imageLoader:ImageLoader
#State var image:UIImage = UIImage()
init(withURL url:String) {
imageLoader = ImageLoader(urlString:url)
}
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.top)
.onReceive(imageLoader.didChange) {
data in self.image = UIImage(data: data) ?? UIImage()
}
}
The problem I'm having is this is only capable of running once, If i click off the ShowImage1 view and then click back on to it, ImageLoader doesn't run again, and I'm left with a blank page.
How can I ensure that ImageLoader Runs every time the ShowImage1 view is accessed?
EDIT:
I access ShowImage1 like this:
struct PostCallForm: View {
var body: some View {
NavigationView {
Form {
Section {
Button(action: {
if true {
self.showImage1 = true
}
}){
Text("View Camera 1 Snapshot")
}.overlay(NavigationLink(destination: ShowImage1(withURL: "example.com/1.jpg"), isActive: self.$showImage1, label: {
EmptyView()
}))
}
}
Section {
Button(action: {
}){
Text("Submit")
}
}
}.disabled(!submission.isValid)
}
}
}
import SwiftUI
import Combine
class ImageLoader : ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
func loadImage(urlString:String) {
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
print("imageloader1")
}
}
task.resume()
}
}
struct ShowImage1Parent: View {
#State var url: String = ""
var sampleURLs: [String] = ["https://image.shutterstock.com/image-vector/click-here-stamp-square-grunge-600w-1510095275.jpg", "https://image.shutterstock.com/image-vector/certified-rubber-stamp-red-grunge-600w-1423389728.jpg", "https://image.shutterstock.com/image-vector/sample-stamp-square-grunge-sign-600w-1474408826.jpg" ]
var body: some View {
VStack{
Button("load-image", action: {
url = sampleURLs.randomElement()!
})
ShowImage1(url: $url)
}
}
}
struct ShowImage1: View {
#StateObject var imageLoader:ImageLoader = ImageLoader()
#State var image:UIImage = UIImage()
#Binding var url: String
var body: some View {
VStack{
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.top)
.onReceive(imageLoader.didChange) {
data in self.image = UIImage(data: data) ?? UIImage()
}
.onChange(of: url, perform: { value in
imageLoader.loadImage(urlString: value)
})
}
}
}

Refreshing view by changing #State variable in SwiftUI not working

so for simplicity sake, I am going to first describe the way my code functions. I have a List that is generated by using a ForEach loop to walk through an AWS database.
List{
ForEach(self.data.database1){ row in
HStack {
Button("\((row.name)!)") {
self.selectedItem = row.id
self.selectedItemName = row.poi!
self.pressedItem = true
}
Spacer()
Button("Delete") {
self.selectedItem = row.id
self.showDeleteItemView = true
//this brings up a view that can confirm you want to delete
}.buttonStyle(BorderlessButtonStyle())
}
}
}
Each row in the list contains a "Delete" button. This delete button opens the view seen below:
if self.showDeleteEventView == true {
ConfirmActionView(showView: self.$showDeleteItemView, actionName: "delete this event", function: {
for item in self.data.database1{
if self.selectedItem == item.id{
DispatchQueue.main.async {
self.data.deleteItem(id: event.id)
self.reloadView.toggle()
}
}
}
}, buttonLabel: "Delete")
}
The view is:
struct ConfirmActionView: View {
#Binding var showView: Bool
var actionName: String
var function: () -> Void
var buttonLabel: String
var body: some View {
ZStack {
VStack {
HStack {
Spacer()
Button("X") {
self.showView = false
}
}
Text("Are you sure you want to \(self.actionName)?")
Spacer()
HStack {
Button("\(self.buttonLabel)") {
print("confirmed action")
self.function()
self.showView = false
}
Button("Cancel") {
self.showView = false
}
.padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
}
}.frame(width: 300, height: 150)
}
}
}
The deleteItem() function is the following:
func deleteItem(id: Int) {
let baseUrl = URL(string: itemUrl)
let deletionUrl = baseUrl!.appendingPathComponent("\(id)")
print("Deletion URL with appended id: \(deletionUrl.absoluteString)")
var request = URLRequest(url: deletionUrl)
request.httpMethod = "DELETE"
print(token) // ensure this is correct
request.allHTTPHeaderFields = ["Authorization": "Token \(token)"]
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Encountered network error: \(error)")
return
}
if let httpResponse = response as? HTTPURLResponse {
// this is basically also debugging code
print("Endpoint responded with status: \(httpResponse.statusCode)")
print(" with headers:\n\(httpResponse.allHeaderFields)")
}
// Debug output of the data:
if let data = data {
let payloadAsSimpleString = String(data: data, encoding: .utf8) ?? "(can't parse payload)"
print("Response contains payload\n\(payloadAsSimpleString)")
}
}
task.resume()
}
stateVariable1 is the variable used to hold the name of the item in the row. reloadView is a #State Boolean variable and so I thought if I toggle it, the view should refresh after the item is deleted from the database. The code functions as I imagine EXCEPT the reloadView toggle doesn't actually reload the view.
You need a dynamic variant of ForEach if your data can change.
Try replacing:
List {
ForEach(self.data.database1) { row in
HStack {
...
with:
List {
ForEach(self.data.database1, id: \.id) { row in
HStack {
...
EDIT
It also looks like your data is not refreshed after you delete an item - you delete it from the server, but not locally.
You can reload it after you delete an item:
func deleteItem(id: Int) {
...
let task = URLSession.shared.dataTask(with: request) { data, response, error in
...
if let data = data {
let payloadAsSimpleString = String(data: data, encoding: .utf8) ?? "(can't parse payload)"
print("Response contains payload\n\(payloadAsSimpleString)")
}
// here you can reload data from the server
// or pass function (which reloads data) as a parameter and call it here
}
task.resume()
}