Swiftui view doesn't refresh when navigated to from a different view - ios

I have, what is probably, a beginner question here. I'm hoping there is something simple I'm missing or I have done wrong.
I essentially have a view which holds a struct containing an array of id strings. I then have a #FirestoreQuery which accesses a collection which holds objects with these id's. My view then displays a list with two sections. One for the id's in the original struct, and one for the remaining ones in the collection which don't appear in the array.
Each listitem is a separate view which displays the details of that item and also includes a button. When this button is pressed it adds/removes that object from the parent list and the view should update to show that object in the opposite section of the list from before.
My issue is that this works fine in the 'preview' in xcode when I look at this view on it's own. However if I run the app in the simulator, or even preview a parent view and navigate to this one, the refreshing of the view doesn't seem to work. I can press the buttons, and nothing happens. If i leave the view and come back, everything appears where it should.
I'll include all the files below. Is there something I'm missing here?
Thanks
Main view displaying the list with two sections
import SwiftUI
import FirebaseFirestoreSwift
struct SessionInvitesView: View {
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#Binding var sessionViewModel : TrainingSessionViewModel
#State private var searchText: String = ""
#State var refresh : Bool = false
var enrolledClients : [Client] {
return clients.filter { sessionViewModel.session.invites.contains($0.id!) }
}
var availableClients : [Client] {
return clients.filter { !sessionViewModel.session.invites.contains($0.id!) }
}
var searchFilteredClients : [Client] {
if searchText.isEmpty {
return availableClients
} else {
return availableClients.filter {
$0.dogName.localizedCaseInsensitiveContains(searchText) ||
$0.name.localizedCaseInsensitiveContains(searchText) ||
$0.dogBreed.localizedCaseInsensitiveContains(searchText) }
}
}
var backButton: some View {
Button(action: { self.onCancel() }) {
Text("Back")
}
}
var body: some View {
NavigationView {
List {
Section(header: Text("Enrolled")) {
ForEach(enrolledClients) { client in
SessionInviteListItem(client: client, isEnrolled: true, onTap: removeClient)
}
}
Section(header: Text("Others")) {
ForEach(searchFilteredClients) { client in
SessionInviteListItem(client: client, isEnrolled: false, onTap: addClient)
}
}
}
.listStyle(.insetGrouped)
.searchable(text: $searchText)
.navigationTitle("Invites")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: backButton)
}
}
func removeClient(clientId: String) {
self.sessionViewModel.session.invites.removeAll(where: { $0 == clientId })
refresh.toggle()
}
func addClient(clientId: String) {
self.sessionViewModel.session.invites.append(clientId)
refresh.toggle()
}
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
}
struct SessionInvitesView_Previews: PreviewProvider {
#State static var model = TrainingSessionViewModel()
static var previews: some View {
SessionInvitesView(sessionViewModel: $model)
}
}
List item view
import SwiftUI
struct SessionInviteListItem: View {
var client : Client
#State var isEnrolled : Bool
var onTap : (String) -> ()
var body: some View {
HStack {
VStack(alignment: .leading) {
HStack {
Text(client.dogName.uppercased())
.bold()
Text("(\(client.dogBreed))")
}
Text(client.name)
.font(.subheadline)
}
Spacer()
Button(action: { onTap(client.id!) }) {
Image(systemName: self.isEnrolled ? "xmark.circle.fill" : "plus.circle.fill")
}
.buttonStyle(.borderless)
.foregroundColor(self.isEnrolled ? .red : .green)
}
}
}
struct SessionInviteListItem_Previews: PreviewProvider {
static func doNothing(_ : String) {}
static var previews: some View {
SessionInviteListItem(client: buildSampleClient(), isEnrolled: false, onTap: doNothing)
}
}
Higher level view used to navigate to this list view
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionEditView: View {
// MARK: - Member Variables
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#StateObject var sheetManager = SheetManager()
var mode: Mode = .new
var dateManager = DateManager()
#State var viewModel = TrainingSessionViewModel()
#State var sessionDate = Date.now
#State var startTime = Date.now
#State var endTime = Date.now.addingTimeInterval(3600)
var completionHandler: ((Result<Action, Error>) -> Void)?
// MARK: - Local Views
var cancelButton: some View {
Button(action: { self.onCancel() }) {
Text("Cancel")
}
}
var saveButton: some View {
Button(action: { self.onSave() }) {
Text("Save")
}
}
var addInviteButton : some View {
Button(action: { sheetManager.showInvitesSheet.toggle() }) {
HStack {
Text("Add")
Image(systemName: "plus")
}
}
}
// MARK: - Main View
var body: some View {
NavigationView {
List {
Section(header: Text("Details")) {
TextField("Session Name", text: $viewModel.session.title)
TextField("Location", text: $viewModel.session.location)
}
Section {
DatePicker(selection: $sessionDate, displayedComponents: .date) {
Text("Date")
}
.onChange(of: sessionDate, perform: { _ in
viewModel.session.date = dateManager.dateToStr(date: sessionDate)
})
DatePicker(selection: $startTime, displayedComponents: .hourAndMinute) {
Text("Start Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: startTime, perform: { _ in
viewModel.session.startTime = dateManager.timeToStr(date: startTime)
})
DatePicker(selection: $endTime, displayedComponents: .hourAndMinute) {
Text("End Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: endTime, perform: { _ in
viewModel.session.endTime = dateManager.timeToStr(date: endTime)
})
}
Section {
HStack {
Text("Clients")
Spacer()
Button(action: { self.sheetManager.showInvitesSheet.toggle() }) {
Text("Edit").foregroundColor(.blue)
}
}
ForEach(viewModel.session.invites, id: \.self) { clientID in
self.createClientListElement(id: clientID)
}
.onDelete(perform: deleteInvite)
}
Section(header: Text("Notes")) {
TextField("Add notes here...", text: $viewModel.session.notes)
}
if mode == .edit {
Section {
HStack {
Spacer()
Button("Delete Session") {
sheetManager.showActionSheet.toggle()
}
.foregroundColor(.red)
Spacer()
}
}
}
}
.navigationTitle(mode == .new ? "New Training Session" : "Edit Training Session")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
leading: cancelButton,
trailing: saveButton)
.actionSheet(isPresented: $sheetManager.showActionSheet) {
ActionSheet(title: Text("Are you sure?"),
buttons: [
.destructive(Text("Delete Session"), action: { self.onDelete() }),
.cancel()
])
}
.sheet(isPresented: $sheetManager.showInvitesSheet) {
SessionInvitesView(sessionViewModel: $viewModel)
}
}
}
func createClientListElement(id: String) -> some View {
let client = clients.first(where: { $0.id == id })
if let client = client {
return AnyView(ClientListItem(client: client))
}
else {
return AnyView(Text("Invalid Client ID: \(id)"))
}
}
func deleteInvite(indexSet: IndexSet) {
viewModel.session.invites.remove(atOffsets: indexSet)
}
// MARK: - Local Event Handlers
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
func onSave() {
self.viewModel.onDone()
self.dismiss()
}
func onDelete() {
self.viewModel.onDelete()
self.dismiss()
self.completionHandler?(.success(.delete))
}
// MARK: - Sheet Management
class SheetManager : ObservableObject {
#Published var showActionSheet = false
#Published var showInvitesSheet = false
}
}
struct TrainingSessionEditView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionEditView(viewModel: TrainingSessionViewModel(session: buildSampleTrainingSession()))
}
}
I'm happy to include any of the other files if you think it would help. Thanks in advance!

Related

How to update filtered list in swiftui, when the value in the filter is changed?

Usual caveat of being new to swiftui and apologies is this is a simple question.
I have a view where I have a date picker, as well as two arrows to increase/decrease the day. When this date is update, I am trying to filter a list of 'sessions' from the database which match the currently displayed date.
I have a filteredSessions variable which applies a filter to all 'sessions' from the database. However I do not seem to have that filter refreshed each time the date is changed.
I have the date to be used stored as a "#State" object in the view. I thought this would trigger the view to update whenever that field is changed? However I have run the debugger and found the 'filteredSessions' variable is only called once, and not when the date is changed (either by the picker or the buttons).
Is there something I'm missing here? Do I need a special way to 'bind' this date value to the list because it isn't directly used by the display?
Code below. Thanks
import SwiftUI
struct TrainingSessionListView: View {
#StateObject var viewModel = TrainingSessionsViewModel()
#State private var displayDate: Date = Date.now
#State private var presentAddSessionSheet = false
private var dateManager = DateManager()
private let oneDay : Double = 86400
private var addButton : some View {
Button(action: { self.presentAddSessionSheet.toggle() }) {
Image(systemName: "plus")
}
}
private var decreaseDayButton : some View {
Button(action: { self.decreaseDay() }) {
Image(systemName: "chevron.left")
}
}
private var increaseDayButton : some View {
Button(action: { self.increaseDay() }) {
Image(systemName: "chevron.right")
}
}
private func sessionListItem(session: TrainingSession) -> some View {
NavigationLink(destination: TrainingSessionDetailView(session: session)) {
VStack(alignment: .leading) {
Text(session.title)
.bold()
Text("\(session.startTime) - \(session.endTime)")
}
}
}
private func increaseDay() {
self.displayDate.addTimeInterval(oneDay)
}
private func decreaseDay() {
self.displayDate.addTimeInterval(-oneDay)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
decreaseDayButton
Spacer()
DatePicker("", selection: $displayDate, displayedComponents: .date)
.labelsHidden()
Spacer()
increaseDayButton
Spacer()
}
.padding(EdgeInsets(top: 25, leading: 0, bottom: 0, trailing: 0))
Spacer()
ForEach(filteredSessions) { session in
sessionListItem(session: session)
}
Spacer()
}
.navigationTitle("Training Sessions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: addButton)
.sheet(isPresented: $presentAddSessionSheet) {
TrainingSessionEditView()
}
}
}
var filteredSessions : [TrainingSession] {
print("filteredSessions called")
return viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }
}
}
struct TrainingSessionListView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionListView()
}
}
There are two approaches and for your case and for what you described I would take the first one. I only use the second approach if I have more complex filters and tasks
You can directly set the filter on the ForEach this will ensure it gets updated whenever the displayDate changes.
ForEach(viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }) { session in
sessionListItem(session: session)
}
Or you can like CouchDeveloper said, introduce a new state variable and to trigger a State change you would use the willSet extension (doesn't exist in binding but you can create it)
For this second option you could do something like this.
Start create the Binding extension for the didSet and willSet
extension Binding {
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
let snapshot = self.wrappedValue
self.wrappedValue = $0
execute(snapshot)
}
)
}
func willSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute($0)
self.wrappedValue = $0
}
)
}
}
Introduce the new state variable
#State var filteredSessions: [TrainingSession] = []
// removing the other var
We introduce the function that will update the State var
func filterSessions(_ filter: Date) {
filteredSessions = viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: date) }
}
We update the DatePicker to run the function using the willSet
DatePicker("", selection: $displayDate.willSet { self.filterSessions($0) }, displayedComponents: .date)
And lastly we add a onAppear so we fill the filteredSessions immidiatly (if you want)
.onAppear { filterSessions(displayDate) } // uses the displayDate that you set as initial value
Don't forget in your increaseDay() and decreaseDay() functions to add the following after the addTimeInterval
self.filterSessions(displayDate)
As I said, this second method might be better for more complex filters
Thank you all for your responses. I'm not sure what the issue was originally but it seems updating my view to use Firebase's #FirestoreQuery to access the collection updates the var filteredSessions... much better than what I had before.
New code below seems to be working nicely now.
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionListView: View {
#FirestoreQuery(collectionPath: "training_sessions") var sessions : [TrainingSession]
#State private var displayDate: Date = Date.now
#State private var presentAddSessionSheet = false
private var dateManager = DateManager()
private let oneDay : Double = 86400
private var addButton : some View {
Button(action: { self.presentAddSessionSheet.toggle() }) {
Image(systemName: "plus")
}
}
private var todayButton : some View {
Button(action: { self.displayDate = Date.now }) {
Text("Today")
}
}
private var decreaseDayButton : some View {
Button(action: { self.decreaseDay() }) {
Image(systemName: "chevron.left")
}
}
private var increaseDayButton : some View {
Button(action: { self.increaseDay() }) {
Image(systemName: "chevron.right")
}
}
private func sessionListItem(session: TrainingSession) -> some View {
NavigationLink(destination: TrainingSessionDetailView(sessionId: session.id!)) {
VStack(alignment: .leading) {
Text(session.title)
.bold()
Text("\(session.startTime) - \(session.endTime)")
}
}
}
private func increaseDay() {
self.displayDate.addTimeInterval(oneDay)
}
private func decreaseDay() {
self.displayDate.addTimeInterval(-oneDay)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
decreaseDayButton
Spacer()
DatePicker("", selection: $displayDate, displayedComponents: .date)
.labelsHidden()
Spacer()
increaseDayButton
Spacer()
}
.padding(EdgeInsets(top: 25, leading: 0, bottom: 10, trailing: 0))
if filteredSessions.isEmpty {
Spacer()
Text("No Training Sessions found")
} else {
List {
ForEach(filteredSessions) { session in
sessionListItem(session: session)
}
}
}
Spacer()
}
.navigationTitle("Training Sessions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: todayButton, trailing: addButton)
.sheet(isPresented: $presentAddSessionSheet) {
TrainingSessionEditView()
}
}
}
var filteredSessions : [TrainingSession] {
return sessions.filter { $0.date == dateManager.dateToStr(date: displayDate)}
}
}
struct TrainingSessionListView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionListView()
}
}

Refresh List After a New Entity is Added in Core Data for SwiftUI App

I am building a small app using SwiftUI and Core Data. I have a main view, which launches the sheet. The sheet allows me to add a new movie to the SQLite database through Core Data. But I am having a hard time to refresh the parent view once the sheet is dismissed.
ContentView
struct ContentView: View {
#State private var isPresented: Bool = false
#StateObject private var vm = MovieListViewModel()
var body: some View {
NavigationView {
VStack {
List(vm.movies) { movie in
Text(movie.title)
}
}
.navigationTitle("Movies")
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add Movie") {
isPresented = true
}
}
})
.sheet(isPresented: $isPresented, content: {
AddMovieView()
})
.onAppear {
try? vm.populateMovies()
}.padding()
}
}
}
AddMovieView
struct AddMovieView: View {
#Environment(\.dismiss) private var dismiss
#StateObject private var vm = AddMovieViewModel()
var body: some View {
Form {
TextField("Title", text: $vm.title)
Button("Save") {
do {
try vm.saveMovie()
dismiss()
} catch {
print(error.localizedDescription)
}
}
}
}
}
Do I need to call vm.populateMovies() on the onDismiss function of the sheet from the ContentView?
You can use a #FetchRequest as follows:
struct ContentView: View {
#FetchRequest(sortDescriptors: []) var movies: FetchedResults<Movie>
#State private var isPresented: Bool = false
#StateObject private var vm = MovieListViewModel()
var body: some View {
NavigationView {
VStack {
List(movies) { movie in
Text(movie.title)
}
}
.navigationTitle("Movies")
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add Movie") {
isPresented = true
}
}
})
.sheet(isPresented: $isPresented, content: {
AddMovieView()
})
.padding()
}
}
}
You won't need a populateMovies() as the FetchRequest result is automatically populated.

EmptyBody().onAppear not works when sheet present with item

When I present sheet with .sheet(isPresented... onAppear of EmptyView() triggered
but when I use .sheet(item... then onAppear doesn't trigger. I don't understand what mistake I am doing?
item:
enum ActiveSheet: Identifiable {
var id: String { UUID().uuidString }
case customA
case customB
}
Main View:
struct ContentView: View {
#State private var activeSheet: ActiveSheet?
var body: some View {
VStack {
Button(action: { activeSheet = .customA }) {
Text("View A")
}
Button(action: { activeSheet = .customB }) {
Text("View B")
}
}
.buttonStyle(.borderedProminent)
//If I use this .sheet(isPresented... then onAppear triggers, but not with item
.sheet(item: $activeSheet) { item in
switch item {
case .customA:
CustomViewA()
case .customB:
CustomViewB()
}
}
}
}
Empty Views:
struct CustomViewA: View {
var body: some View {
EmptyView()
.onAppear {
print("OnAppear")
}
}
}
struct CustomViewB: View {
var body: some View {
EmptyView()
.onAppear {
print("OnAppear")
}
}
}

SwiftUI - Present 3 different Views with different parameter

I need to present 3 different Views.
AddListView
ChangeColor
EditListView
They take different paramater. AddListView does not have parameter while ChangeColor and EditListView takes Color and NSManagedObject respectively. However for the sake of simplicity, EditListView's paramter is integer in this example.
I am using .fullScreenCover(item: <#T##Binding<Identifiable?>#>, content: <#T##(Identifiable) -> View#>) for presenting them.
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
}
else if type == .EditListView {
if let index = selectedIndex {
EditListView(index: index)
}
}
else if type == .ChangeColor {
if let color = selectedColor {
ColorView(color: color)
}
}
}
selectedIndex and selectedColor is nil even though I initialize them before initializing presentedViewType. And hence, an EmptyView is presented.
This is the project.
enum PresentedViewType: Identifiable {
case AddListView
case ChangeColor
case EditListView
var id: Int {
return hashValue
}
}
struct ContentView: View {
#State var presentedViewType: PresentedViewType?
#State var selectedColor: Color?
#State var selectedIndex: Int?
var body: some View {
NavigationView {
List {
Section {
NavigationLink(destination: Text("All")) {
Text("All")
}
.background(Color.blue)
.contextMenu {
Button(action: {
selectedColor = .blue
presentedViewType = .ChangeColor
}) {
Label("Change Color", systemImage: "paintbrush.pointed.fill")
}
}
}
ForEach(0..<10) { index in
NavigationLink(destination: Text("Row Details \(index)")) {
Text("Row \(index)")
}
.contextMenu {
Button(action: {
selectedIndex = index
presentedViewType = .EditListView
}) {
Label("Edit", systemImage: "square.and.pencil")
}
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
presentedViewType = .AddListView
}) {
Label("Add", systemImage: "plus")
}
}
}
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
}
else if type == .EditListView {
if let index = selectedIndex {
EditListView(index: index)
}
}
else if type == .ChangeColor {
if let color = selectedColor {
ColorView(color: color)
}
}
}
}
}
}
struct ColorView: View {
#Environment(\.presentationMode) var presentationMode
#State var color: Color
var body: some View {
NavigationView {
Text("Color View")
.background(color)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
struct AddListView: View {
#Environment(\.presentationMode) var presentationMode
#State var text: String = ""
var body: some View {
NavigationView {
TextField("", text: $text)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
struct EditListView: View {
#Environment(\.presentationMode) var presentationMode
#State var index: Int
var body: some View {
NavigationView {
Text("Row \(index)")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
I have to mention that they do not have fixed value. They have different value depending on which row you need to edit.
How to pass selectedIndex and selectedColor to EditListView and ColorView respectively?
Update
EditListView takes only selectedIndex while ColorView takes only selectedColor
You need to have #Binding properties inside EditListView and ColorView
struct EditListView: View {
#Binding var selectedIndex: Int?
// rest of view implementation
}
struct ColorView: View {
#Binding var selectedIndex: Int?
// rest of view implementation
}
and then pass the binding in the initialisers
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
} else if type == .EditListView {
EditListView(index: $selectedIndex)
} else if type == .ChangeColor {
ColorView(color: $selectedColor)
}
}

UI changes with ObservableObject just after switching tabs

I have a ObservableObject that I use to update my UI when new data is sent from the server (an a class which contains an array of custom structs).
For some reason, when the data is sent, the ContentView's body is called, but the data isn't changed. I even added a print statement to check if the data that the array contains is right and it is.
When I try to switch to another tab on my TabView, and then switch back to the main view, the UI does get updated. Does anybody know why the UI updates just when I switch tabs, although the body gets recalled to update the UI when the data changed?
HomeView
struct HomeView: View {
#ObservedObject private var fbData = firebaseData
var body: some View {
TabView {
//Home Tab
NavigationView {
ScrollView(showsIndicators: false) {
ForEach(self.fbData.posts.indices, id: \.self) { postIndex in
PostView(post: self.$fbData.posts[postIndex])
.listRowInsets(EdgeInsets())
.padding(.vertical, 5)
}
}
.navigationBarTitle("MyPhotoApp", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
print("Camera btn pressed")
}, label: {
Image(systemName: "camera")
.font(.title)
})
, trailing:
Button(action: {
print("Messages btn pressed")
}, label: {
Image(systemName: "paperplane")
.font(.title)
})
)
} . tabItem({
Image(systemName: "house")
.font(.title)
})
Text("Search").tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
}
Text("Upload").tabItem {
Image(systemName: "plus.app")
.font(.title)
}
Text("Activity").tabItem {
Image(systemName: "heart")
.font(.title)
}
Text("Profile").tabItem {
Image(systemName: "person")
.font(.title)
}
}
.accentColor(.black)
.edgesIgnoringSafeArea(.top)
}
}
FirebaseData:
class FirebaseData : ObservableObject {
#Published var posts = [Post]()
let postsCollection = Firestore.firestore().collection("Posts")
init() {
self.fetchPosts()
}
//MARK: Fetch Data
private func fetchPosts() {
self.postsCollection.addSnapshotListener { (documentSnapshot, err) in
if err != nil {
print("Error fetching posts: \(err!.localizedDescription)")
return
} else {
documentSnapshot!.documentChanges.forEach { diff in
if diff.type == .added {
let post = self.createPostFromDocument(document: diff.document)
self.posts.append(post)
} else if diff.type == .modified {
self.posts = self.posts.map { (post) -> Post in
if post.id == diff.document.documentID {
return self.createPostFromDocument(document: diff.document)
} else {
return post
}
}
} else if diff.type == .removed {
for index in self.posts.indices {
if self.posts[index].id == diff.document.documentID {
self.posts.remove(at: index)
}
}
}
}
}
}
}
Your code example doesn't help to find the bug. Finally I've got how to demonstrate it. First, do it the "proper way" (copy - paste - try it yourself)
import SwiftUI
struct Data: Identifiable {
let id = UUID()
let text: String
}
class Model: ObservableObject {
#Published var data: [Data] = [Data(text: "alfa"), Data(text: "beta")]
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
TabView {
View1(model: model).tabItem {
Text("View 1")
}
View2(model: model).tabItem {
Text("View 2")
}
}
}
}
struct View1: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text("View 1").font(.largeTitle)
DataView(data: model.data)
Button(action: {
self.model.data.append(Data(text: String("ABCDEFGH".shuffled())))
}) {
Text("Add random data")
}
}
}
}
struct View2: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text("View 2").font(.largeTitle)
DataView(data: model.data.filter({ (item) -> Bool in
item.text.count < 4
}))
// to distinguish from other DataView !! it seems to be a bug in SwiftUI
// try to remove it to see the difference
.id("view2")
Button(action: {
self.model.data.append(Data(text: String("ABC".shuffled())))
}) {
Text("Add random data")
}
}
}
}
struct DataView: View {
var data: [Data]
var body: some View {
List {
ForEach(data) { (item) in
Text(item.text)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I works as it should, you can modify the data from each tab, you see refreshed data, etc.
removing "fixed - user defined" .id modifier, it changes the behaviour dramatically
This looks like a serious bug in SwiftUI ...
I think is SwiftUI bug. I solve this problem like this.
Instead of rendering your PostView(post: self.$fbData.posts[postIndex])
implement post view inside ForEach.
ForEach(self.fbData.posts.indices, id: \.self) { postIndex in
Text(self.$fbData.posts[postIndex].comment)
Text(self.$fbData.posts[postIndex].date)
....
}

Resources