SwiftUI ForEach loop not updating from ObservableObject - swiftui

I started playing with SwiftUI in the last few days and it makes a lot of sense.
However, I'm struggling when it comes to updating a persisting list.
I'm using UserDefaults and JSONEncoder.
struct ContentView: View {
private var slackController = Slack()
#EnvironmentObject var userData: UserData
var body: some View {
return NavigationView {
List {
ForEach(self.userData.statuses) { myStatus in
HStack {
// row code
}
.onTapGesture {
self.slackController.setStatus(text: myStatus.description, emoji: myStatus.emoji)
}
}
.onDelete(perform: delete)
}
}
}
func delete(at offsets: IndexSet) {
self.userData.statuses.remove(atOffsets: offsets)
}
Both insert and delete are working, because when I relaunch the app I can see the changes.
private let defaultStatuses: [Status] = [
Status(emoji: ":spiral_calendar_pad:", description: "In a meeting", expireHours: 1),
Status(emoji: ":car:", description: "Commuting", expireHours: 12),
]
final class UserData: ObservableObject {
let didChange = PassthroughSubject<UserData, Never>()
#UserDefault(key: "ApiToken", defaultValue: "")
var apiToken: String {
didSet {
didChange.send(self)
}
}
#UserDefault(key: "Statuses", defaultValue: defaultStatuses)
var statuses: [Status] {
didSet {
didChange.send(self)
}
}
}
I can see that didChange.send(self) is called, but I can't figure out why the list isn't updated.
The code is also in a repo in case I've missed something from the example that is useful.
I've also been using SwiftUITodo to try figure out I'm doing wrong.
Any help/guideance would be appreciated.

Related

SwiftUI insert, delete, move and select with smooth animation

I am planning to implement following features in the SwiftUI list - delete, insert, move and select.
With the existing list I am able to delete a row. But can't select a row does not work with List(selection: self.$selectedObject). When I hit edit it always enters into delete mode. And I comment the delete code nothing happens when I tap on edit button. This the first problem.
Also, selectedObject can it be moved to Model instead of keeping it with the ContentView?
Like UITableView, I am not able to get the insert green button. Is it like SwiftUI does not support the green insert button?
Overall trying to understand how the insert, delete, move and select functionality can work with the List SwiftUI.
Another problem I have noticed is that animation is very fast and not smooth when it enters into edit mode (with delete actions).
struct ContentView: View {
#StateObject private var model = Model()
#State var selectedObject: Locations?
var body: some View {
NavigationView {
List(selection: self.$selectedObject) {
ForEach(model.identifiableLocations) { location in
Text(location.name)
}
.onDelete(perform: delete(of:))
}.listStyle(.plain)
.navigationTitle("Places")
.toolbar {
EditButton()
Button {
model.addLocation(name: "Test")
} label: {
Image(systemName: "plus")
}
}
}
}
func delete(of indexSet: IndexSet){
indexSet.forEach { index in
model.delete(itemAt: index)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().previewDevice(PreviewDevice(rawValue: "iPhone 14"))
}
}
extension ContentView {
#MainActor class Model: ObservableObject {
#Published private(set) var identifiableLocations = [Locations(name: "USA"),
Locations(name: "Switzerland")]
}
}
extension ContentView.Model {
func addLocation(name: String) {
identifiableLocations.append(Locations(name: name))
}
func delete(itemAt index: Int) {
identifiableLocations.remove(at: index)
}
}
struct Locations {
var name: String
}
extension Locations: Identifiable,Hashable {
var id: String {
return UUID().uuidString
}
}
to make selection work, the list cells need a .tag(). This value is going into the selection var.
yes, selectedObject can be moced to the view model as an additional #Published var
SwiftUI List does not have an insert method, but your Add Button already does that.
The animation is broke because your id in Location is not stable, but generated on each call by the computed var. id should be stable!
Here a running code with comments:
#MainActor
class ViewModel: ObservableObject {
#Published private(set) var identifiableLocations = [
Locations(name: "USA"),
Locations(name: "Switzerland")
]
// published selection var
#Published var selectedObject: Locations?
func addLocation(name: String) {
identifiableLocations.append(Locations(name: name))
}
func delete(itemAt index: Int) {
identifiableLocations.remove(at: index)
}
// new move func
func move(fromOffsets: IndexSet, toOffset: Int) -> Void {
identifiableLocations.move(fromOffsets: fromOffsets, toOffset: toOffset)
}
}
struct Locations: Identifiable, Hashable {
let id = UUID() // id has to stay stable
// var id: String {
// return UUID().uuidString
// }
var name: String
}
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
// #State var selectedObject: Locations? // is now in viewmodel
var body: some View {
NavigationView {
List(selection: $viewModel.selectedObject) {
ForEach(viewModel.identifiableLocations) { location in
Text(location.name)
.tag(location) // this makes selction work
}
.onDelete(perform: delete(of:))
.onMove(perform: viewModel.move)
}
.listStyle(.plain)
.navigationTitle("Places")
.toolbar {
EditButton()
Button {
viewModel.addLocation(name: "Test")
} label: {
Image(systemName: "plus")
}
}
}
}
func delete(of indexSet: IndexSet){
indexSet.forEach { index in
self.viewModel.delete(itemAt: index)
}
}
}

Firebase List Constantly Refreshing

I am still trying to figure out swiftui. I am writing a program that utilizes a database for a grocery app. I decided to go with Google Firebase and so far so good. The issue I have though is I am trying to load a list of products and this list is constantly refreshing. When I scroll it refreshes and I am back at the top of the list. I was wondering if I could get some help as to what I am doing wrong here. I will include my code below and try to best explain. Thanks in advance!
struct ContentView: View {
#State var selectedIndex = 0
var body: some View {
VStack {
Button( action: {
selectedIndex = 5
} label: {
Image(systemName: "magnyfyingglass")
}
}
switch selectedIndex {
case 0:
// some code
case 1:
// some code
case 2:
// some code
case 3:
// some code
case 4:
// some code
case 5:
SearchView()
default
// some code
}
}
}
SearchView looks like this:
struct SearchView: View {
#State private var searchText = ""
#ObservedObject var listModel = InvListView()
var body: some View {
NavigationView {
List {
ForEach(self.listModel.invList.filter{(self.searchText.isEmpty ? true : $0.description.localizedCaseInsensitiveContains(self.searchText))}, id: \.id) {products in
NavigationLink(destination: Detail(data: products)) {
Text(products.description)
}
}
}
.searchable(text: self.$searchText)
{
ForEach(listModel.invList, id:\.id) {info in
HStack {
Text(info.description)
.searchCompletion(info.description)
}
}
}
}
}
}
struct Detail: View {
var data: InventoryList
var body: some View {
VStack {
Text(data.description)
Text(data.category)
}.padding()
}
}
InventoryList
import Foundation
import Firebase
class InvListView: ObservableObject {
#Published var invList = [InventoryList]()
init() {
// Access inventory in the database
let database = Firestore.firestore()
database.collection("inventory").getDocuments { snapshot, error in
if error != nil {
// Errors will fix later
return
}
if let snapshot = snapshot {
DispatchQueue.main.async {
self.invList = snapshot.documents.map { d in
return InventoryList(id: d.documentID,
upc: d["upc"] as? Int ?? 0,
description: d["description"] as? String ?? "",
category: d["category"] as? String ?? "",
price: d["price"] as? Double ?? 0.0,
url: d["imageUrl"] as? String ?? "")
}
}
}
}
}
}
struct InventoryList: Identifiable {
var id: String
var upc: Int
var description: String
var category: String
var price: Double
var url: String
}
I hope this is enough to go on. I think it has something to do either with the switch or the init but not sure how to fix it.
I was able to figure it out on my own. The #ObservedObject in SearchView was causing the view to refresh. I changed it to #StateObject and that seemed to fix the problem. There were some small bugs after, but once I removed them everything else worked.

SwiftUI: #State property not updated without weird workaround

I'm experiencing strange behavior with an #State property that isn't being properly updated in its originating view after being changed in another view. I'm using Xcode 12.3 and iOS 14.
What happens is that an #State "session" value-based item and #State "flow" value-based item are sent as bound parameters to another view. When a button is tapped there, it changes their values, and a fullScreenCover call in the originating view is supposed to get the correct view to display next in the flow from a switch statement. But the "session" item is nil in that switch statement unless I include an onChange modifier that looks for changes in either of the two #State properties. The onChange call doesn't have to have any code in it to have this effect.
I'm still relatively new to SwiftUI (although fairly experienced with iOS and Mac development). But this is confusing the heck out of me. I don't understand why it isn't working as expected, nor why adding an empty onChange handler makes it work.
If you'd like to experience this for yourself, here's code to assemble a simple demo project:
// the model types
struct ObservationSession: Codable {
public let id: UUID
public var name: String
public init(name: String) {
self.name = name
self.id = UUID()
}
}
struct SessionListModals {
enum Flow: Identifiable {
case configuration
case observation
case newSession
var id: Flow { self }
}
}
// ContentView
struct ContentView: View {
#State private var mutableSession: ObservationSession?
#State private var flow: SessionListModals.Flow?
var body: some View {
VStack {
Button("New Session", action: {
mutableSession = ObservationSession(name: "")
flow = .newSession
})
.padding()
}
.fullScreenCover(item: $flow) {
viewForFlow($0)
}
// Uncomment either of these 2 onChange blocks to see successful execution of this flow
// Why does that make a difference?
// .onChange(of: mutableSession?.name, perform: { value in
// //
// })
// .onChange(of: flow, perform: { value in
// //
// })
}
#ViewBuilder private func viewForFlow(_ flow: SessionListModals.Flow) -> some View {
switch flow {
case .newSession:
// MARK: - Show New Session View
NavigationView {
NewSessionView(session: $mutableSession, flow: $flow)
.navigationTitle("Create a session")
.navigationBarItems(leading: Button("Cancel", action: {
self.flow = nil
}))
}
case .observation:
// MARK: - Show RecordingView
NavigationView {
let name = mutableSession?.name ?? "Unnamed session"
RecordingView(sessionName: name)
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
default:
NavigationView {
EmptyView()
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// NewSessionView
struct NewSessionView: View {
#Binding var session: ObservationSession?
#Binding var flow: SessionListModals.Flow?
var body: some View {
VStack {
Text("Tap button to create a new session")
Button("New Session", action: {
createNewSession()
})
.padding()
}
}
private func createNewSession() {
let newSession = ObservationSession(name: "Successfully Created A New Session")
session = newSession
flow = .observation
}
}
struct NewSessionView_Previews: PreviewProvider {
static let newSession = ObservationSession(name: "Preview")
static let flow: SessionListModals.Flow = .newSession
static var previews: some View {
NewSessionView(session: .constant(newSession), flow: .constant(flow))
}
}
// RecordingView
struct RecordingView: View {
var sessionName: String
var body: some View {
Text(sessionName)
}
}
struct RecordingView_Previews: PreviewProvider {
static var previews: some View {
RecordingView(sessionName: "Preview")
}
}
class ObservationSession: //Codable, //implement Codable manually
ObservableObject {
public let id: UUID
//This allows you to observe the individual variable
#Published public var name: String
public init(name: String) {
self.name = name
self.id = UUID()
}
}
struct SessionListModals {
enum Flow: Identifiable {
case configuration
case observation
case newSession
var id: Flow { self }
}
}
// ContentView
class ContentViewModel: ObservableObject {
#Published var mutableSession: ObservationSession?
}
struct ContentView: View {
//State stores the entire object and observes it as a whole it does not individually observe its variables that is why .onChange works
#StateObject var vm: ContentView3Model = ContentView3Model()
#State private var flow: SessionListModals.Flow?
var body: some View {
VStack {
Button("New Session", action: {
//Since you want to change it programatically you have to put them in another object
vm.mutableSession = ObservationSession(name: "")
flow = .newSession
})
.padding()
}
.fullScreenCover(item: $flow) {
viewForFlow($0)
}
}
#ViewBuilder private func viewForFlow(_ flow: SessionListModals.Flow) -> some View {
switch flow {
case .newSession:
// MARK: - Show New Session View
NavigationView {
NewSessionView(session: $vm.mutableSession, flow: $flow)
.navigationTitle("Create a session")
.navigationBarItems(leading: Button("Cancel", action: {
self.flow = nil
}))
}
case .observation:
// MARK: - Show RecordingView
NavigationView {
let name = vm.mutableSession?.name ?? "Unnamed session"
RecordingView(sessionName: name)
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
default:
NavigationView {
EmptyView()
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
}
}
}

Index out of range shrinking an array using ForEach and NavigationLink in SwiftUI [duplicate]

I've seen several posts about this, but so far none of the solutions seem to be working for me.
I'm trying to create an array of Identifiable items using ForEach -- with both a Text() and Toggle() view inside. The array is stored in a #Published property of an #ObservableObject.
I'm currently looping through the indices to create the toggle bindings (as suggested in other posts).
Everything appears to be working, until I try to delete a row.
(Specifically the last row - which triggers a "Fatal error: Index out of range" every time.)
Any help would be greatly appreciated!
struct Rule: Identifiable {
let id: String
var displayName: String
var isEnabled: Bool
}
class UserData: ObservableObject {
#Published var rules: [Rule] = []
}
struct RuleListView: View {
#ObservableObject var userData: UserData
var body: some View {
List {
ForEach(userData.rules.indices, id: \.self) { index in
HStack {
Toggle(
isOn: self.$userData.rules[index].isEnabled
) { Text("Enabled") }
Text(self.userData.rules[index].displayName)
}
}
.onDelete(perform: delete)
}
}
func delete(at offsets: IndexSet) {
userData.rules.remove(atOffsets: offsets)
}
}
It seems you have complicated your code:
class UserData: ObservableObject {
#Published var rules: [Rule] = []
}
Will notice when new element is added to rules array, you could have done that just by declaring:
#State var rules = [Rule]()
You probably want to know when isEnabled in Rule class changes. Right now it is not happening. For that to ObservableObject must be the Rule class.
Keeping that in mind, if you change your code to:
import SwiftUI
class Rule: ObservableObject, Identifiable {
let id: String
var displayName: String
#Published var isEnabled: Bool
init(id: String, displayName: String, isEnabled: Bool) {
self.id = id
self.displayName = displayName
self.isEnabled = isEnabled
}
}
struct ContentView: View {
// for demonstration purpose, you may just declare an empty array here
#State var rules: [Rule] = [
Rule(id: "0", displayName: "a", isEnabled: true),
Rule(id: "1", displayName: "b", isEnabled: true),
Rule(id: "2", displayName: "c", isEnabled: true)
]
var body: some View {
VStack {
List {
ForEach(rules) { rule in
Row(rule: rule)
}
.onDelete(perform: delete)
}
}
}
func delete(at offsets: IndexSet) {
rules.remove(atOffsets: offsets)
}
}
struct Row: View {
#ObservedObject var rule: Rule
var body: some View {
HStack {
Toggle(isOn: self.$rule.isEnabled)
{ Text("Enabled") }
Text(rule.displayName)
.foregroundColor(rule.isEnabled ? Color.green : Color.red)
}
}
}
It will notice when new element is added to rules array, and also will notice when isEnabled changes.
This also solves your problem with crashing.

Is there a way to remove a row in a list in SwiftUI?

I have created a quiet simple list in SwiftUI and want to make it editable, like a tableView in UIKit. I want to remove a row in the list with the all known gesture (swipe from, the right to the left).
I have tried to make with a button above the list, but it doesn't look nice an is not practicable for my app.
struct singleIsland: Identifiable {
let id: Int
let name:String
}
var islands = [
singleIsland(id: 0, name: "Wangerooge"),
singleIsland(id: 1, name: "Spiekeroog"),
singleIsland(id: 2, name: "Langeoog")
]
var body: some View {
VStack {
List(islands) { island in
Text(island.name)
}
}
}
struct SingleIsland {
let name: String
}
struct ContentView: View {
#State var islands = [
SingleIsland(name: "Wangerooge"),
SingleIsland(name: "Spiekeroog"),
SingleIsland(name: "Langeoog")
]
var body: some View {
List {
ForEach(islands.identified(by: \.name)) { island in
Text(island.name)
}.onDelete(perform: delete)
}
}
private func delete(with indexSet: IndexSet) {
indexSet.forEach { islands.remove(at: $0) }
}
}
Wrapping the data in a #State makes sure the view is redrawn if it changes.
Note:
I'm getting compilers errors if the List is built this way:
List(data) { item in
[...]
}
It will complain that onDelete does not exist for the List.
My workaround is to use a ForEach inside the List, and the onDelete function on it.
Yes, this is very straight forward with SwiftUI.
Updating your code like this...
struct SingleIsland: Identifiable {
let id: Int
let name:String
}
struct IslandListView: View {
#State private var islands = [
SingleIsland(id: 0, name: "Wangerooge"),
SingleIsland(id: 1, name: "Spiekeroog"),
SingleIsland(id: 2, name: "Langeoog")
]
var body: some View {
List {
ForEach(islands.identified(by: \.name)) { island in
Text(island.name)
}.onDelete(perform: delete)
}
}
func delete(at offsets: IndexSet) {
islands.remove(at: offsets)
}
}
This will allow your view to swipe to delete rows.
Using #State sets up your view to depend on the islands array. Any update to that array will trigger the view to reload. So by deleting an item from the array it will animate the change to the list.
Add this to the list:
.onDelete { $0.forEach { islands.remove(at: $0) } }
After you turning islands into an #State
you can not do that with a static list.
in the real world your list of islands would probably come from the outside of your view anyway.
we use your struct:
[...]
struct singleIsland: Identifiable {
var id: Int
var name:String
}
[...]
and create an bindable Object to hold those islands
[...]
class IslandStore : BindableObject {
let didChange = PassthroughSubject<IslandStore, Never>()
var islands : [singleIsland] {
didSet { didChange.send(self) }
}
init (islands: [singleIsland] = []){
self.islands = islands
}
}
[...]
you need to import combine to use BindableObject
[...]
import SwiftUI
import Combine
[...]
your view now binds the island store
the .onDelete(perform: delete) automatically adds the swipe left to delete function. We have to code the delete function tho:
[...]
struct ForTesting : View {
#ObjectBinding var store = IslandStore()
var body: some View {
List {
ForEach(store.islands) { island in
Text(island.name)
}.onDelete(perform: delete)
}
}
func delete(at offsets: IndexSet) {
// theres seems to be a bug that prevents us from using atOffsets
// so we convert to index
guard let index = Array(offsets).first else { return }
store.islands.remove(at: index)
}
}
[...]
and while we are at it we add a EditButton() and a title. We have to wrap our list in a NavigationView to do this
[...]
struct ForTesting : View {
#ObjectBinding var store = IslandStore()
var body: some View {
NavigationView {
List {
ForEach(store.islands) { island in
Text(island.name)
}.onDelete(perform: delete)
}
.navigationBarTitle(Text("Islands"))
.navigationBarItems(trailing: EditButton())
}
}
func delete(at offsets: IndexSet) {
// theres seems to be a bug that prevents us from using atOffsets
// so we convert to index
guard let index = Array(offsets).first else { return }
store.islands.remove(at: index)
}
}
[...]
change your DEBUG section to initialise the islands store and hand it over to your view:
#if DEBUG
var islands = [
singleIsland(id: 0, name: "Wangerooge"),
singleIsland(id: 1, name: "Spiekeroog"),
singleIsland(id: 2, name: "Langeoog"),
singleIsland(id: 3, name: "Baltrum")
]
struct ForTesting_Previews : PreviewProvider {
static var previews: some View {
ForTesting(store: IslandStore(islands:islands))
}
}
#endif
complete code