SwiftUI Update NavigationView > List > ForEach with .refreshable? - swift

I have code with NavigationView and List view that contains a ForEach with NavigationLink. My problem is in the view when user pressing button save with user info id, username etc. in ContentView that info appears only after refreshing app, so my question what logic I can put in .refreshable modifier?
#ObservedObject var habit = Habit()
...
ForEach(0..<habit.items.count, id: .self) { index in
NavigationLink(destination: HabitView(habit: self.habit, index: index)) {
...
I think that this link demonstrates one possible solution, for another way, but it exceeds my skills in programming: https://troz.net/post/2019/swiftui-data-flow/ .
I think in this situation is a simpler solution than to change all of the code.

Related

SwiftUI ForEach.onDelete properly animates but then resets

Trying to figure out why my ForEach loop isn’t updating. I have the model marked as ObservedObject and have done everything I could to make sure the updates were happening. I even saw that the model was being updated while printing.
class Model {
var array: [Int]
}
…
struct ModelView: View {
#ObservedObject var model: Model
var body: some View {
List {
ForEach(model.array.indices, id:\.self) { index in
…
}.onDelete(perform: delete)
}
}
The row is animating and acting like it is deleting and does delete in the model, however the deleted row animates back in and the original data set is shown!
I figured it out. I needed to make the array in my model object with the #Published property wrapper. Doing that fixed everything!

How to change Number of Columns of SwiftUI NavigationView depending on Selected SideBar Item?

I am currently working with the SwiftUI NavigationView on the iPad.
Problem: Obviously iPadOS supports 2 or 3 columns NavigationViews. However, I have to declare the number of Columns when initializing the NavigationView without any chance of changing this later.
My Idea (Does not work):
#State var selection: NavigationItem? = .first
NavigationView {
sidebar
Text("Select an Option in the left Sidebar.")
if (selection == .first) {
Text("Choose an Element to preview it here.")
}
}
The NavigationView on the iPad will always be a 3 Columns One, although I only pass the third Text when the Selection == .first.
Question: How can I accomplish the Goal of having a NavigationView, which Number of Columns depend on the selected Option in the Sidebar. For Example, when selecting the First Option, I would like to have a 3 Column NavigationView while having a 2 Column One when the Second Option is selected?
Thanks for your help.

How can I prevent SwiftUI from reinitializing my wrapped property just like it does with #StateObject?

From what I've read whenever you instantiate an object yourself in your view, you should use #StateObject instead of #ObservedObject. Because apparently if you use #ObservedObject, SwiftUI might decide in any moment to throw it away and recreate it again later, which could be expensive for some objects. But if you use #StateObject instead then apparently SwiftUI knows not to throw it away.
Am I understanding that correctly?
My question is, how does #StateObject communicate that to SwiftUI? The reason I'm asking is because I've made my own propertyWrapper which connects a view's property to a firebase firestore collection and then starts listening for live snapshots. Here's an example of what that looks like:
struct Room: Model {
#DocumentID
var id: DocumentReference? = nil
var title: String
var description: String
static let collectionPath: String = "rooms"
}
struct MacOSView: View {
#Collection({ $0.order(by: "title") })
private var rooms: [Room]
var body: some View {
NavigationView {
List(rooms) { room in
NavigationLink(
destination: Lazy(ChatRoom(room))
) {
Text(room.title)
}
}
}
}
}
The closure inside #Collection is optional, but can be used to build a more precise query for the collection.
Now this works very nicely. The code is expressive and I get nice live-updating data. You can see that when the user would click on a room title, the navigation view would navigate to that chat room. The chatroom is a view which shows all the messages in that room. Here's a simplified view of that code:
struct ChatRoom: View {
#Collection(wait: true)
private var messages: [Message]
// I'm using (wait: true) to say "don't open a connection just yet,
// because I need to give you more information that I can't give you yet"
// And that's because I need to give #Collection a query
// based on the room id, which I can only do in the initializer.
init(_ room: Room) {
_messages = Collection { query in
query
.whereField("room", isEqualTo: room.id!)
.order(by: "created")
}
}
var body: some View {
List(messages) { message in
MessageBubble(message: message)
}
}
}
But what I've noticed, is that SwiftUI initializes a new messages-collection every single time the user interacts with the UI in any way. Like even when an input box is clicked to give it focus. This is a huge memory leak. I don't understand why this happens, but is there a way to tell SwiftUI to only initialize #Collection once, just like it does with #StateObject?
Use #StateObject when you want to define a new source-of-truth reference type owned by that View and tied to its life-cycle. The object will be created just before the body is run for the first time and is stored in a special place by SwiftUI. If the View struct is recreated (e.g. by a parent View's body recompute by a state change) then the previous object will be set on the property instead of a new object. If the View is no longer init during body updates then the object will be deinit.
When you pass a #StateObject into a child View then in that child View uses #ObservedObject to enable the body of the child view to be recomputed when the object changes, just the same way the body of the parent View will also be recomputed because it used an #StateObject. If you use #ObservedObject for an object that was not an #StateObject in a parent View then since no View is owning it then it will be lost every time the View is init during body updates. Also, any objects that you create in the View init, like your Collection, those are immediately lost too. These excessive heap allocations could cause a leak and slows down SwiftUI. Objects the View owns must be wrapped in #StateObject to avoid this.
Lastly, don't use #StateObject for view state and certainly don't try and implement a legacy "View Model" pattern with them. We have #State and #Binding for that. #StateObject is only for model objects (as in your domain types: Book, Person etc.), loaders or fetchers.
WWDC 2020 Data Essentials in SwiftUI explains all this very nicely.

SwiftUI macOS Scroll a List With Arrow Keys While a TextField is Active

I'd like to use a SwiftUI TextField and a SwiftUI List to render a "search box" above a list of items. Something roughly like the search box available in Safari's Help menu item...which provides a search box where you can always enter text while simultaneously browsing through the list of results using the up and down arrow keys.
I've played with onMoveCommand, focusable, and adjustments to the "parent" NSWindow, but haven't found a clear and obvious way for the TextField to constantly accept input while still being able to navigate the underlying List using the up and down arrow keys. The following code allows for either text to be entered in the TextField, or list entries to be navigated through, but not both at the same time...
struct ContentView: View {
#State var text: String = ""
#State var selection: Int? = 1
var body: some View {
VStack {
TextField("Enter text", text: $text)
List(selection: $selection) {
ForEach((1...100), id: \.self) {
Text("\($0)")
}
}
}
}
}
You can wrap a NSTextField in a NSViewRepresentable and use the NSTextFieldDelegate to intercept the move up and down keys. You can take a look into my suggestions demo.
Source of a text field with suggestions

Why is my SwiftUI List row not always updating internally

I have a list of reminders grouped into sections by completion and date. With data coming from an ObservedObject DataStore called global. I pass a realmBinding to the cell. The cell can update this binding and it will trigger the data store to update.
List {
// Past Due
if self.global.pastDueReminders.count > 0 {
Section(header: SectionHeader {}){
ForEach(self.global.pastDueReminders) { reminder in
NavigationLink(destination: ReminderDetail( reminder: reminder.realmBinding())) {
GeneralReminderCell(reminder: reminder.realmBinding())
}
}
}
}
// Completed
if self.global.completeReminders.count > 0 {
// Same as PastDue but for Completed
}
}
The cell looks something like:
struct GeneralReminderCell: View {
#Binding var reminder:Reminder
var body: some View {
HStack(alignment:.top, spacing: 10) {
Image(systemName: reminder.completed ? "checkmark.circle.fill" : "circle")
.onTapGesture(perform:{ self.reminder.completed = !self.reminder.completed })
VStack(alignment: .leading, spacing: 2) {
Text("Follow up with \(reminder.client.fullName)").fontWeight(.semibold)
if reminder.title.count > 0 {
Text(reminder.title)
}
Text(reminder.date.formatted()).foregroundColor(.gray)
}
}.padding(.vertical, 10)
}
}
When tapping on an image it toggles the reminder completion state and its position changes in the List view. The image that was tapped should changed to a filled in check when completed.
This behaviour almost always happens as expected, but sometimes the checked image will get out of sync with the completed state of reminder. I've look at this for quite some time and have not made much headway. Why is the checked image not always matching the state of the data?
Even though this is a very old question, I may have been working on what appears to be this same problem. In my case, my App is running on macOS. At first, the problem also seemed to be very intermittent and had been difficult to reproduce consistently.
I also have a View with a ForEach supplying rows to a List. My row's View contains an #State for an Optional Image that gets updated several different ways via actions performed by that same row View (e.g. Continuity Camera, file picker or drag & drop). The issue is that sometimes the new Image is shown and sometimes it is not shown. Using Self._printChanges() I am able to see that the #State is changing and the row's body it is being redrawn, however, the rendered View does not change. The only pattern that I am able to observe is that this issue only seems to occur with the last row in the List. Based on the success of my workaround below, it seems to confirm that there is an issue with the way SwiftUI's List reuses table cells.
My solution/workaround is to replace:
List {
ForEach {
}
}
With:
ScrollView {
LazyVStack {
ForEach {
}
}
}