In several places in my app, I implement a paged loading scheme using a ScrollView and a LazyVStack, with an ProgressView after the loaded items. The ProgressView is only shown if the model has more items to load, and on appear of the ProgressView, loading of the next page is triggered.
The new items get appended to the list in the model, and they get rendered, pushing the ProgressView off the bottom of the screen. When the user scrolls up, the ProgressView reappears, and triggers loading of the next page.
For some reason, in one list where I’m using a LazyVGrid inside a LazyVStack, when the next page loads, the entire view scrolls such that the bottom of the stack is still displayed, causing the ProgressView to sit there spinning. It’s as if the ScrollView is pinning the scroll to the bottom of its content, and I can’t figure out why.
The view looks like this:
ScrollView
{
LazyVStack
{
LazyVGrid(columns: self.columns, spacing: 20.0)
{
if let products = self.collection.products,
products.count > 0
{
ForEach(products)
{ inProduct in
NavigationLink(destination: ProductDetailsContainerView(searchResult: WNProductSearchResult(product: inProduct)))
{
ProductListingCell(imageURL: inProduct.imageURL,
title: inProduct.name,
listingCount: inProduct.availableCount,
price: inProduct.marketPrice)
}
}
}
}
if let products = self.collection.products,
products.count == 0
{
self.emptyDisplay
}
// Progress loader triggers loading…
if self.collection.hasMore
{
HStack
{
Spacer()
ProgressView()
Spacer()
}
.padding(.top, 40)
.onAppear { self.collection.load(next: 20) }
}
}
.padding(20)
}
Even more bafflingly, this only occurs after the second page loads. The first page loads as expected; then you scroll to the bottom, the ProgressView is revealed, the second page loads, and the items fill in without moving the scroll position. If you then scroll up again, the ProgressView is revealed, the third page loads, and the items fill in, but the scroll view is repositioned such that you’re looking at the new bottom of the list.
Hopefully you can see the behavior:
Related
To my knowledge, the only way to assign a navigation bar a background color that is separate from the rest of the screen, you set the background color to whatever object you have flush with the navigation view. (In this case, its a divider with the background set to red)
My intention is to then place a view flush to the divider in an attempt to create some sort of "subtitle view". The problem is, as you can see, there is a space between my Stack and my Divider... I'm not sure what is causing this space and I am not sure how to get rid of it.
My first thought was, perhaps there is some safe area being adhered to... That said, I tried ignoring that, but that didn't work.
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
Divider()
.background(.red)
.navigationTitle("Main Title")
// There is a space here
ZStack {
Rectangle().frame(height: 35)
Text("Subtitle View")
.foregroundColor(.white)
}.edgesIgnoringSafeArea([.top, .bottom])
Spacer()
}.background(.blue)
}
}
}
I use a TabView to simulate a horizontal scrolling paged list. This work fine. But I want to change the background, according to the currently active tab. I utilise .onAppear() for this, but this results in the images not being in sync with the item in the current tab (see video: https://youtube.com/shorts/aQxrErRmCi8?feature=share)
Basically, it should only change the background whenever the tab is 'fully' selected and centered on screen. Not as soon as the next or previous tab comes onto screen just a few pixels.
Secondly, I want to be able to set the selected programmatically, for instance after I insert a new object, I want to select the first tab. How Can I achieve that. The tabs have the id of my item (game) as tag.
Relevant code:
#State private var backgroundImage = AppImages.gameDefaultBackgroundImage
#ViewBuilder var tabList: some View {
TabView {
ForEach(gamesViewModel.games) { game in
GameCardView(game: game)
.tag(game.id)
.onAppear {
withAnimation(.easeInOut) {
backgroundImage = gamesViewModel.getGameImageCoreData(for: game)
}
}
.listRowBackground(Color.clear)
.frame(width: getScreenRectangle().width - 20)
.onTapGesture { showAnnotationsView(for: game) }
.onLongPressGesture(minimumDuration: 0.15) { gameDetailsView(for: game) }
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
If more code is needed, please ask ...
I have an array of objects (employees) that I am displaying in a navigationview. Each object has a boolean property called "Active". If the employee is active I want the navigationlink for that list item to work as it normally would but if the employee is not active then I want that list item to be disabled so that the navigationlink or any swipe actions do not work. This is my code:
NavigationView {
List {
CustomSearchBar(searchText: $searchText, searchCategory: $searchCategory)
ForEach(Employees) { person in
ZStack {
NavigationLink(destination: DisplayDetails().environmentObject(person)) {
ListItem().environmentObject(person)
}
}
.swipeActions(edge: .leading, content: {
Button {
Employees.toggleArchiveFlag(for: person.id)
} label: {
Label("Archive", systemImage: !person.archived ? "square.and.arrow.down" : "trash")
}
.tint(!person.archived ? .blue : .red)
})
.disabled(!person.active)
}
}
.navigationTitle("Current Employees")
.padding(.horizontal,-15) // remove the padding that the list adds to the screen
}
What ends up happing is that when the view initially loads everything is enabled regardless of each employee's active status. But if I click any of the navigationlinks to load the "DisplayDetails" detailed view and then return back to the main navigationview OR if I click on any of the searchbar toggles or use the searchbar to filter my list of people then the view updates and the correct people are disabled.
It is almost as if the statement ".disabled(!person.active)" is being called too late. If that is the case then where should I be calling it? I have tried moving that statement in the following places:
The closing bracket of the Zstack. But this does nothing
Right below the "ListItem().environmentObject(person)" statement but this still shows the same behavior as mentioned earlier and when the navigationlink is eventually disabled then the swipeactions are still enabled.
Any help at all would be appreciated!
Figured out that the issue was with the logic that set the person.active boolean value not with the presentation of the navigation view items. Thanks.
I have a horizontal ScrollView on top of a MapView.
The ScorllView is a collection of Buttons. It is weird that the buttons in the ScrollView are sometime tapable and sometimes not. First tap always works but after that I have to scroll a bit, tap around different areas in the button, make some secret prayers and then it works!
I tried disabling/removing all other components in the view, but still unable to figure out the root cause.
Has anyone experience this ?
I stuck with a same issue with horizontal ScrollView on top and List. While debugging I added empty .onTapGesture to ScrollView and it somehow fix my issue.
VStack(spacing: 0) {
ScrollView(.horizontal) {
HStack {
Button("one") {}
Button("two") {}
Button("three") {}
}
}
.onTapGesture { // <---- fix
}
List {
}
}
I also faced the same issue for Horizontal Scroll view in Swiftui dark theme "CameraTimerItem" buttons not clickable (Problem with dark theme only). Then I put a onTapGesture without any action. It's starts to work normally. I think it's a error of SwiftUI.
VStack (alignment:.center){
ScrollView(.horizontal, showsIndicators: false) {
HStack{
ForEach(timeSlots,id: \.self) { item in
CameraTimerItem(cellTitle: item)
}
}
.frame(width: AppUtils.width, alignment: .center)
}
.onTapGesture {
// <---- This is the solution
}
}
To anyone else having this issue, instead of adding an empty .onTapGesture view modifier, check that any HStacks in the ScrollView hierarchy have a .contentShape(Rectangle()) modifier. By default, HStacks don't accept taps in between their child views, and depending on your child view's layout this can cause taps to be missed even when it looks like they should be landing. .contentShape(Rectangle()) makes the entire frame of the HStack tappable.
This is my example that I am trying to get to work:
struct ContentView: View {
let links = ["Item 1", "Item 2", "Item 3", "Item 4"]
var body: some View {
NavigationView {
ScrollView {
Text("My Title")
List(links, id: \.self) {
link in
NavigationLink(destination: TestView()) {
Text(link)
.padding(.vertical, 4)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.frame(height: 178)
Text("Some more content here")
}
}
}
}
Note: TestView is just some view with the text hello world on it.
I am trying to copy Apple Music's style of navigation. I tried putting a Button in the NavigationLink but tapping it on the text wouldn't change views, and I couldn't find a way to reliably change the color of the row when tapped, at the same time. Also in some approach, I managed to make it work, but the way the colors animate is different, i.e. it fades from A to B, over ~100ms whereas what I'm trying to achieve is to animate between the states instantly (like in Apple Music).
My current approach is using a List, putting NavigationLinks inside it and then cutting off the whole view by giving it a height. This way I can put it alongside other content.
It's working fine for now, but whenever I click on an row and go back, the row is still highlighted, when it shouldn't. Is there a way to make it so that it deselects when going back to the screen somehow?
I think this bug is being caused by the List being inside a ScrollView, since when I removed ScrollView, the list worked properly, and there wasn't this highlight bug. But I need to be able to put my content with the list, and I don't intend to have a list take up the whole screen.
Is there any way to fix this bug with this approach? I'm also willing for other ways to achieve the same result without using List.
Trying to use ForEach instead ofList?
With a view for row (CustomRow) where you can pass link item and set custom dividing line, background etc ...
ForEach(links, id: \.self) { link in
NavigationLink(destination: TestView()) {
CustomRow(item: link)
}
}
.frame(height: 178)