SwiftUI Empty List Auto Refresh - swiftui

I have a list of data from MongoDB Realm which is being displayed as a list on my View, however I want to show the Text("No list available") when the Realm has no data in it. I implemented the same with an if - else loop, however when a data is added, the list still shows "No list available", and doesn't listen for the changes immediately, I have to restart the app for the list to be seen. How do I get my view to listen for a change and then display either the list with values or "No list available" then itself?
This is my code:
#State var data = try! Realm().objects(Diary.self).filter("userID = '\(uid!)'")
if data.isEmpty {
Text("No Data")
} else {
List{
ForEach(data) { value in
Section(header: Text(value.date!)){
VStack {
HStack {
Text(value.title!)
Spacer()
}
.padding(.bottom, 5)
HStack {
Text(value.mood!)
Spacer()
}
}
}
}
}
}

Related

How to trigger a NavigationLink programmatically in a LazyVGrid

I have a LazyVGrid inside a NavigationView.
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(items) { item in
NavigationLink(tag: item, selection: $displayedItem) {
DetailView(item)
} label: {
GridItemView(item)
}
}
}
}
}
The referenced variables are defined as follows on the view:
#State var displayedItem: Item?
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
Now I want to show the detail view for a specific item. I do this by simply assigning this item to the displayedItem property:
func showDetailView(for item: Item) {
displayedItem = item
}
This works great when the respective item is visible on the LazyVGrid at the moment when I call this function. However, when the item is not visible, I first need to scroll to the item for the NavigationLink to fire. I know why this is happening (because the items are loaded lazily, it's a lazy grid after all), but I don't know how to make the LazyVGrid load the specific item when I need it.
What I've tried:
I have also tried to programmatically scroll to the target item by wrapping the entire ScrollView inside a ScrollViewReader and appending the following modifier:
.onChange(of: displayedItem) { item in
if let item = item {
scrollProxy.scrollTo(item.id)
}
}
Unfortunately, this has the same problem: Scrolling to a given item doesn't work until the item is loaded.
Question:
Is there any way to make this work, i.e. to trigger a NavigationLink for an item that is not currently visible in the LazyVGrid? (It's important for me as I need this functionality to deep-link to a specific item's DetailView.)
An possible approach can be like in this topic - use one link somewhere in background of ScrollView and activate it by tapGesture/button from user or assigning corresponding value programmatically.
Tested with Xcode 13.4 / iOS 15.5
Main part:
ScrollView {
LazyVGrid(columns: columns) {
ForEach(items) { item in
RoundedRectangle(cornerRadius: 16).fill(.yellow)
.frame(maxWidth: .infinity).aspectRatio(1, contentMode: .fit)
.overlay(Text("Item \(item.value)"))
.onTapGesture {
selectedItem = item
}
}
}
}
.padding(.horizontal)
.background(
NavigationLink(destination: DetailView(item: selectedItem), isActive: isActive) {
EmptyView()
}
)
.toolbar {
Button("Random") { selectedItem = items.randomElement() }
}
Test module on GitHub

SwiftUI: how to have image in list view not have same action as list item?

I have a list inside a view. Inside the list, items are iterated through to populate the list. When you click on each list item, I want to navigate to another view.
This is working as expected but I want to have a Button represented by a circle in each list item that can be clicked independently without navigating to the second view. Right now, clicking the circle just takes me to 2nd view. How can I accomplish this?
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(
destination: OtherView(name: "test"),
label: {
Text("Navigate")
})
HStack {
Image("1")
.resizable()
.frame(width: 32.0, height: 32.0)
Button(action: addItem) {
Label("Add Item", systemImage: "circle")
}
}
}
}
}
}
You can not do this with List. You can use VStack or LazyVStack inside a ScrollView as an alternative solution

SwiftUI Add view below a list

I have a list with some items.
Below the list I'd like to have to button to load more items.
(As loading all items requires some user actions like entering a TAN, this should not be done automatically when the user scrolls to the end of the list, but only if he likes to.)
What I'd like to have is a view like this:
However, if I place the List and the Button in a VStack, the Button get always displayed at the bottom of the screen, not only when I scroll to the end of the List:
struct ContentView: View {
private let items = Range(0...15).map { "Item " + String($0) }
var body: some View {
VStack {
List(items, id: \.self) { item in
Text(item)
}
HStack {
Spacer()
Button("Load more") { print("Load more items") }
Spacer()
}
}
}
}
If I add the Button to the List, the Button obviously gets displayed as a List item with a white background and without any space to the list:
struct ContentView: View {
private let items = Range(0...15).map { "Item " + String($0) }
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
HStack {
Spacer()
Button("Load more") { print("Load more items") }
Spacer()
}
}.listStyle(GroupedListStyle())
}
}
Is there any way to add a view that becomes visible when the user scrolls to the end of the List but that is not part of the List? (Or at least looks like being below the List and not part of it?)
You should use second variant, but a bit tuned, like below (colors/spaces modify per your needs
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
HStack {
Button("Load more") { print("Load more items") }
}
.listRowInsets(EdgeInsets())
.frame(maxWidth: .infinity, minHeight: 60)
.background(Color(UIColor.systemGroupedBackground))
}.listStyle(GroupedListStyle())
}

SwiftUI List with ForEach works in one VStack but not in second one

I'm at a complete loss for why a List I'm trying to create (with a ForEach) in SwiftUI is behaving the way that it is. I have an ObservedObject that I verified in the inspector has the data that I'm expecting. In the first VStack, I can create the List with the ForEach and it outputs just fine. In the second VStack, I get no data output.
struct WorkoutPreview: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var workoutSessionExercises: WorkoutSessionExercises
var body: some View {
NavigationView {
VStack {
Text("Welcome to your workout! Below are the exercises and bands you've selected for each.")
.padding()
Text("We found \(workoutSessionExercises.workoutExercises.count) workouts.")
.padding()
// This List (with ForEach) works fine and produces output.
List {
ForEach(0..<workoutSessionExercises.workoutExercises.count) { index in
Text(self.workoutSessionExercises.workoutExercises[index].exerciseName)
}
}
}
// The exact same List (with ForEach) in this VStack produces no results.
VStack {
List {
ForEach(0..<workoutSessionExercises.workoutExercises.count) { index in
Text(self.workoutSessionExercises.workoutExercises[index].exerciseName)
}
}
}
Spacer()
}
}
}
the problem is you have 2 VStacks that are not one below the other in the View, and so you can't see the second list.
To fix your problem wrap your VStacks in another VStack, such as:
var body: some View {
NavigationView {
VStack { // <------
VStack {
Text("Welcome to your workout! Below are the exercises and bands you've selected for each.").padding()
Text("We found \(workoutSessionExercises.workoutExercises.count) workouts.").padding()
// This List (with ForEach) works fine and produces output.
List {
ForEach(0..<workoutSessionExercises.workoutExercises.count) { index in
Text(self.workoutSessionExercises.workoutExercises[index].exerciseName)
}
}
}
// The exact same List (with ForEach) in this VStack produces no results.
VStack {
List {
ForEach(0..<workoutSessionExercises.workoutExercises.count) { index in
Text(self.workoutSessionExercises.workoutExercises[index].exerciseName)
}
}
}
Spacer()
} // <------
}
}

Button and List interaction modifications

I am trying to remove the "sectionStyle" for my List rows. In order to achieve a tableview-like view, you would write the following:
List(items, id: \.id) { item in
ItemRow(with: item)
}
If you want to interpret tap events on each row, you would wrap the ItemRow into a Button:
List(items, id: \.id) { item in
Button(action: rowTapped) {
ItemRow(item: item)
}
}
The interesting part is that when the List knows about the Button, it will automatically give a selection animation like tableView's selectionStyle. For my application, I'd like to not have that. Is there a way to set the "selectionStyle" for the List to "none"?
There are a number of List behaviours, that I haven't been able to alter. It's quite easy to build your own list though if you have specific needs different than the system List view. Here's a simple example that I've used.
struct MyListView:View {
var body: some View {
ScrollView {
ForEach(items, id: \.id) { item in
VStack {
Button(action: self.rowTapped) {
ItemRow(item: item)
}
Divider()
}
}
}.padding()
}
func rowTapped() {
print("rowTapped")
}
}
struct ItemRow:View {
var item:Item
var body: some View {
HStack {
Text(item.name)
Spacer() // force text to left justify
}
}
}