ProgressView hides on list scroll - ios

I have a List with a ProgressView and some rows. When I scroll the List down and up again, the ProgressView get hidden, I notice this bug only with a certain number of rows, for example on iPhone 13 you can reproduce this bug if you have 20 rows.
struct ContentView: View {
var body: some View {
List {
ProgressView()
ForEach(0..<20, id: \.self) {
Text("\($0)")
}
}
}
}

I know that it's probably too late. But I also had this problem and figured it might be caused by SwiftUI "recovering" your previously used view that (for some reason) it's now hidden.
So what I did to fix this was just adding a unique ID to my progress view, this way SwiftUI won't try to re-utilize it.
Ex:
ProgressView().id(UUID())

Related

SwiftUI dismisses the detail view when an NSManagedObject subclass instance changes in a way that moves it to a different section of a fetch request?

The sample app is a default SwiftUI + Core Data template with two modifications. Detail for Item is a separate view where the user can change the timestamp. And sectioned fetch request is used as opposed to a regular one.
#SectionedFetchRequest(
sectionIdentifier: \.monthAndYear, sortDescriptors: [SortDescriptor(\.timestamp)]
) private var items: SectionedFetchResults<String, Item>
var body: some View {
NavigationView {
List(items) { section in
Section(header: Text(section.id)) {
ForEach(section) { item in
NavigationLink {
ItemDetail(item: item)
} label: {
Text(item.timestamp!.formatted())
}
}
}
}
}
}
struct ItemDetail: View {
#ObservedObject var item: Item
#State private var isPresentingDateEditor = false
var body: some View {
Text("Item at \(item.timestamp!.formatted())")
.toolbar {
ToolbarItem(placement: .principal) {
Button(item.timestamp!.formatted()) {
isPresentingDateEditor = true
}
}
}
.sheet(isPresented: $isPresentingDateEditor) {
if let date = Binding($item.timestamp) {
DateEditor(date: date)
}
}
}
}
The problem arises when the user changes the timestamp on an Item to a different month. The detail view dismisses behind the model view arbitrarily. However, the issue is not present if the user changes the timestamp to a day within the same month. It does not matter whether we use a sheet or a full-screen cover. When I was debugging this I noticed that any change of the NSManagedObject subclass instance that will change the section in which it is displayed will dismiss the detail view arbitrarily. I’m expecting to stay on the detail view even if I change the timestamp to a different month.
What is the root cause of this issue and how to fix it?
I think it's because the NavigationLink is no longer active because it has moved to a different section so it has a different structural identity and has defaulted back to inactive. Structural identity is explained in WWDC 2021 Demystify SwiftUI. I reported this as bug FB9977102 hopefully they fix it.
Another major problem with NavigationLink that it doesn't work when offscreen. E.g. in your sample code add lots of rows to fill up more than the screen. Scroll to top, wait one minute (so the picked updates), select first row, adjust the time to current time which will move the row to last in the list and off screen. You'll notice the NavigationLink has deactivated. People face this problem when trying to implement deep links and they can't activate a NavigationLink that is in a row that is off screen.
Update 15th Nov 2022: Apple replied to my feedback FB9977102 "this can be resolved by using the new Navigation APIs to avoid the view identity issues described." NavigationStack seems ok but NavigationSplitView has a row selection bug, see my screen-capture.

What's the problem with picker control in Swift inside a form?

I don't know why the picker is not working inside of a form in Swift UI. The app will be crashing if the user is going to tap for the second time. The console will print the following output:
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window).
struct ContentView: View {
#State private var countryIndex = 0
var countries = ["US", "Germany", "Korea", "Russia"]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $countryIndex, label: Text("Country")) {
ForEach(0 ..< countries.count) {
Text(self.countries[$0]).tag($0)
}
}
}
.navigationBarTitle(Text("Country"))
}
}
}
}
First, the warning you get is just a warning, so you shouldn't mind, but if you set .navigationBarTitle("xyz", displayMode: .inline), it'll go away.
Second, the navigation only working once is an issue only in simulator, if you build and run on a real device, it won't happen.

How do I reset a selection tag when pushing a NavigationLink programmatically?

I want to push a view programmatically instead of relying on the interface that NavigationLink provides (e.g. I want to use a button with no chevron). The correct way is to use NavigationLink with tag and selection, and an EmptyView.
When I attempt to use the following code to push a view, it works to push the view the first time:
struct PushExample: View {
#State private var pushedView: Int? = nil
var body: some View {
NavigationView {
VStack {
Form {
Button(action: { self.pushedView = 1 }) { Text("Push view") }
}
NavigationLink(destination: Text("Detail view"), tag: 1, selection: $pushedView) { EmptyView() }
}
}
}
}
However, if I tap the back button on the view, and try hitting the button again, it no longer pushes the view. This is because the value pushedView is being set to 1 again, but it is already at 1. Nothing is resetting it back to nil upon pop of the Detail view.
How do I get subsequent taps of the button to push the view again?
TL;DR: There is no need to reset the state variable, as SwiftUI will automatically handle it for you. If it's not, it may be a bug with the simulator.
This was a simulator bug on Xcode 11.3!
The way to check if it's a simulator bug is to run an even simpler example:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("Push", destination: Text("Detail"))
}
}
}
On the Xcode 11.3 iPhone 11 Pro Max, this would only work the first time you tap the link.
This worked fine on both a 13.2 and a 13.3 device.
Therefore, when running into odd SwiftUI issues, test on device rather than the simulator.
Update: Restarting the computer didn't fix it either. Thus while SwiftUI is still new, may be better off to use a real device for testing rather than the simulator.

SwiftUI: NavigationLink pops immediately if used within ForEach

I'm using a NavigationLink inside of a ForEach in a List to build a basic list of buttons each leading to a separate detail screen.
When I tap on any of the list cells, it transitions to the detail view of that cell but then immediately pops back to the main menu screen.
Not using the ForEach helps to avoid this behavior, but not desired.
Here is the relevant code:
struct MainMenuView: View {
...
private let menuItems: [MainMenuItem] = [
MainMenuItem(type: .type1),
MainMenuItem(type: .type2),
MainMenuItem(type: .typeN),
]
var body: some View {
List {
ForEach(menuItems) { item in
NavigationLink(destination: self.destination(item.destination)) {
MainMenuCell(menuItem: item)
}
}
}
}
// Constructs destination views for the navigation link
private func destination(_ destination: ScreenDestination) -> AnyView {
switch destination {
case .type1:
return factory.makeType1Screen()
case .type2:
return factory.makeType2Screen()
case .typeN:
return factory.makeTypeNScreen()
}
}
If you have a #State, #Binding or #ObservedObject in MainMenuView, the body itself is regenerated (menuItems get computed again) which causes the NavigationLink to invalidate (actually the id change does that). So you must not modify the menuItems arrays id-s from the detail view.
If they are generated every time consider setting a constant id or store in a non modifying part, like in a viewmodel.
Maybe I found the reason of this bug...
if you use iOS 15 (not found iOS 14),
and you write the code NavigationLink to go to same View in different locations in your projects, then this bug appear.
So I simply made another View that has different destination View name but the same contents... then it works..
you can try....
sorry for my poor English...

Bottom-first scrolling in SwiftUI

How can I make a SwiftUI List start scrolling from the bottom of the screen (like a chat view)?
Ideally, I want to mimic, e.g. the behavior of iMessage when the list updates, meaning it shifts down if an item is added when the user is at the bottom, but holds it’s position if the user manually scrolled up.
The list is read directly from a binding array, and the order can be reversed if convenient.
#komal pointed out that the UITableView (the backend of List) has an atScrollPosition that should provide this functionality. However, there doesn't seem to be a way to access the underlying view without completely reimplementing List as a UIViewRepresentable, which is easier said than done, considering the standard implementation is completely black-boxed and closed-source.
With that said, I've also posted Access underlying UITableView from SwiftUI List, which, if solved, could serve as an answer to this question.
I would suggest that instead of using scrollview, use UITableView as Tablview inherits scroll property of UIScrollview. And you can use "atScrollPosition:UITableViewScrollPositionBottom" to achieve this behavior.
You could try this way of inverting a ScrollView:
struct ContentView: View {
let degreesToFlip: Double = 180
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
ForEach(0..<100) { i in
HStack {
Spacer()
Text("Number: \(i)")
Spacer()
}
.rotationEffect(.degrees(self.degreesToFlip))
}
.rotationEffect(.degrees(self.degreesToFlip))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The problem with this approach is that you inverse everything in the ScrollView so you inverse the content also but the scroll indicator also gets reversed so instead of being on the right side the indicator is now on the left side. Maybe there is some configuration to always have the indicator on the right side.
Note that I have disabled the scroll indicator in this example code and that this also applies on List but in lists I didn't find any ways to hide the scroll indicator.
Hope this helps a little.

Resources