Refreshing view by changing #State variable in SwiftUI not working - swift

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

Related

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 ObservableObject with #Published array

The UI will update when the array is reassigned to a whole new array, but if I perform a remove on the array the UI will not update.
I was thinking maybe since it was still pointing to the same place in memory the UI wouldn’t be notified of the change. However, I added a didSet property observer to print every time the array was changed and the print statement executed on the remove.
I’m at a loss for how to debug further. Any help would be appreciated!
Code:
Here are the views. There is a movie list view which displays a grid of movie views
struct MovieListView: View {
#ObservedObject var viewModel: MovieListViewModel
init(viewModel: MovieListViewModel) {
self.viewModel = viewModel
}
var body: some View {
ZStack {
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: [
GridItem(.flexible(minimum: 100, maximum: 200), alignment: .top),
GridItem(.flexible(minimum: 100, maximum: 200), alignment: .top)
], spacing: 12, content: {
ForEach(0 ..< viewModel.movies.count, id: \.self) { index in
viewModel.viewForMovie(viewModel.movies[index])
}
})
.id(UUID()) //For ignoring animation
}
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color.blue))
.scaleEffect(1.5, anchor: .center)
.opacity(self.viewModel.loadingMovies ? 1 : 0)
}.onAppear { self.viewModel.loadMovies() }
}
}
struct MovieView: View {
#ObservedObject var viewModel: MovieViewModel
var onDelete: (Movie) -> Void
init(viewModel: MovieViewModel, onDelete: #escaping (Movie) -> Void) {
self.viewModel = viewModel
self.onDelete = onDelete
}
var body: some View {
TmdbMovieView(vm: self.viewModel.tmdbViewModel)
.contentShape(RoundedRectangle(cornerRadius: Constants.MovieViews.movieViewCornerRadius, style: .continuous)) //Prevents sharp edges on context menu
.contextMenu {
Button {
self.onDelete(viewModel.movie)
} label: {
Label("Remove from list", systemImage: Constants.Icons.remove)
}
}
}
}
Here's the movie list view model which specifies what view to use for the movies and exposes the movies from the movie list controller
class MovieListViewModel: ObservableObject {
let name: String
var movies: [Movie] {
return self.movieListController.movies
}
#Published var loadingMovies = false
let movieListController: MovieListController
var viewForMovie: (Movie) -> MovieView
init(name: String, movieListController: MovieListController, viewForMovie: #escaping (Movie) -> MovieView) {
self.name = name
self.movieListController = movieListController
self.viewForMovie = viewForMovie
}
func loadMovies() {
DispatchQueue.main.async {
self.loadingMovies = true
}
self.movieListController.getMovies(complete: onLoadMoviesComplete)
}
private func onLoadMoviesComplete(success: Bool) {
if !success {
//TODO: Handle load fail
}
DispatchQueue.main.async {
self.loadingMovies = false
}
}
func deleteMovie(movie: Movie) {
self.movieListController.delete(movie: movie, complete: onDeleteComplete)
}
private func onDeleteComplete(success: Bool) {
if success {
loadMovies() //TODO: Need this because movies already triggered a change, but the view won't update???
} else {
//TODO: Handle delete fail
}
}
}
Here's my view model factory that creates the view model instance and ties the delete callback on the movie view to the view model
class ViewModelFactory {
let movieListController = MovieListController(id: 1)
func makeMovieListViewModel() -> MovieListViewModel {
let viewModel = MovieListViewModel(
name: "Test",
movieListController: self.movieListController,
viewForMovie: { [unowned self] in
MovieView(viewModel: self.makeMovieViewModel(for: $0), onDelete: {_ in})
}
)
viewModel.viewForMovie = { [unowned self] in
MovieView(viewModel: self.makeMovieViewModel(for: $0), onDelete: viewModel.deleteMovie)
}
return viewModel
}
func makeMovieViewModel(for movie: Movie) -> MovieViewModel {
return MovieViewModel(movie: movie)
}
}
And here is the controller which handles actually hitting my api. The get movies function updates the movies array and the UI updates accordingly. However, the delete function does not cause the UI to update
class MovieListController: ObservableObject {
#Published var movies: [Movie] = []
private var id: Int
init(id: Int) {
self.id = id
}
func getMovies(complete: #escaping (Bool) -> Void) {
guard let moviesUrl = URL(string: "\(Constants.Urls.movieLists)/\(id)\(Constants.Urls.moviesPath)") else {
print("Invalid url...")
complete(false)
return
}
URLSession.shared.dataTask(with: moviesUrl) { data, response, error in
guard let data = data else { return }
do {
var decodedMovies = try JSONDecoder().decode([Movie].self, from: data)
decodedMovies.sort { $0.id < $1.id }
self.movies = decodedMovies
complete(true)
} catch {
print("Failed to decode: \(error)")
complete(false)
}
}.resume()
}
func delete(movie: Movie, complete: #escaping (Bool) -> Void) {
guard let deleteMovieUrl = URL(string: "\(Constants.Urls.movieLists)/\(self.id)\(Constants.Urls.moviesPath)/\(movie.id)") else {
print("Invalid url...")
complete(false)
return
}
var request = URLRequest(url: deleteMovieUrl)
request.httpMethod = "DELETE"
URLSession.shared.dataTask(with: request) { data, response, error in
guard let response = response as? HTTPURLResponse,
error == nil else { // check for fundamental networking error
print("error", error ?? "Unknown error")
complete(false)
return
}
guard (200 ... 299) ~= response.statusCode else { // check for http errors
print("statusCode should be 2xx, but is \(response.statusCode)")
print("response = \(response)")
complete(false)
return
}
if let idx = self.movies.firstIndex(where: { $0.id == movie.id }) {
self.movies.remove(at: idx)
}
complete(true)
}.resume()
}
}

Display activity indicator while fetching api data

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

Folder URL from .onDrop() modifier

I want to read the contents of a folder by getting its URL from an .onDrop() modifier.
The starting point is SwiftOnTap's example on how to process an NSItemProvider, but it's not resulting in anything.
How do I extract the URL from an NSItemProvider of UTType "public.url"?
.onDrop(of: ["public.url"], isTargeted: $dropping, perform: { itemProvider in
if let item = itemProvider.first {
item.loadItem(forTypeIdentifier: "public.url", options: nil) { (folder, err) in
if let data = folder as? Data {
let droppedString = String(decoding: data, as: UTF8.self)
print(droppedString)
}
}
}
return true
})
public.url alone doesn't appear to work for folders, but if your constrain it further to public.file-url, it does work. (Tested on macOS 11.2.3):
struct ContentView: View {
#State var dropping = false
var body: some View {
VStack {
Text("Hello, world!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onDrop(of: ["public.file-url"], isTargeted: $dropping, perform: { itemProvider in
if let item = itemProvider.first {
_ = item.loadObject(ofClass: URL.self) { (url, error) in
if let url = url {
print("URL:", url)
}
}
}
return true
})
}
}