SwiftUI ScrollView renders sub views too early before they enter screen - swiftui

I recently started studying ios/swiftui by developing a small application, which has a list of cards to display picture and message loaded from server.
I was using List to hold those cards but the default list 'decoration' (divider, arrow, tapping effect) looks not very good together with my card view. I disable them by adding below code into SceneDelegate:
UITableView.appearance().separatorColor = .clear
UITableViewCell.appearance().selectionStyle = .none
And having some sort of hack to hide the arrow:
List {
ForEach(0..<self.store.data.count, id: \.self) { idx in
NavigationLink(destination: DetailView(item: self.store.data[idx])) {
EmptyView()
}.frame(width: 0).opacity(0)
ItemCard(item: self.store.data[idx])
}
BottomLoader().onAppear {
if self.store.status != .loading {
self.store.load()
}
}
}
But, the problem is that hiding List's selection style and separator color in SceneDelegate applies to all the lists in App, and I do have couple lists (such as the one in settings) need those style.
Then I tried to change the card holder from List to ScrollView but it leads to another trouble, the onAppear callback of last view (BottomLoader) gets called at the same time with ScrollView onAppear gets called.
As far as I understand, onAppear is supposed to be called when view is rendered and shown. List as the item holder, does not have to calculate the full height of all items, that is probably why List renders only the items about to enter screen. But ScrollView does need to know the full height of all items and that is why all the items get rendered.
Below are two small segments to show different behaviors of List and ScrollView:
List {
ForEach(0..<100) { idx in
Text("item # \(idx)").padding().onAppear { print("\(idx) on appear") }
}
}
// only 18 logs printed
And:
ScrollView {
ForEach(0..<100) { idx in
Text("item # \(idx)").padding().onAppear { print("\(idx) on appear") }
}
}
// all 100 logs printed
Is there any way, to let ScrollView acts like List, to render its subviews when they about to enter screen.
Or, is there any way, to disable those default styles to a specific List, not globally?

Related

List animations ignoring Animation parameter

I've been trying to get changes to my list elements to animate correctly. However, items in a list don't seem to animate as specified.
In this simple example, an element is removed. There is an animation, within 1 second the element is removed. However, it completely ignores the duration and delay.
struct ContentView: View {
#State var items = [1, 2, 3, 4, 5]
var body: some View {
VStack {
List {
ForEach(items, id: \.self) { item in
Text("Item \(item)")
}
}
Button {
withAnimation(Animation.easeInOut(duration: 5).delay(1)) {
print("removing element")
items.removeFirst()
}
} label: {
Text("Remove element")
}
}
}
}
If I remove the List and just have a VStack of items, the Animation parameter is processed correctly.
If I remove the withAnimation, it doesn't animate at all. So it is triggering it.
I would say that this is expected as this is how underlying UITableViewController is working, it doesn't have capabilities to customise animations that way.
I think that if you want to do something custom you need to start with LazyVStack in ScrollView, that will give you more space for creation and it is quite likely that this would work (I haven't tried it, but from the logical point it should).

Normal Tap & Swipe actions while in EditMode?

I'm developing an app, one view of which has the primary goal of allowing users to reorder a List of NavigationLinks, but which I would also like to allow navigation & a few other things. I want users to be able to:
Reorder by dragging on the reorder control.
Navigate to the link by tapping elsewhere on the row.
Swipe from the leading edge to activate a swipe action.
At the moment, enabling EditMode (to allow reordering) disables navigation & swipe actions; I haven't been able to find a workaround that allows all 3 functions simultaneously. Is there a good way to do this?
Here's an example:
struct ReorderableListView: View {
var items: [X] // List of custom objects
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: CustomView(item)) { // winds up disabled by edit mode
Text(item.name)
}
.swipeActions(edge: .leading) { Button("Swipe") {print("swipe")} } // winds up disabled by edit mode
}
.onMove { from, to in
print("move \(from) to \(to)")
}
}
.environment(\.editMode, .constant(.active)) // always in edit mode for reordering
}
}
}
Update:
Based on the accepted answer, it looks like this behavior got updated in iOS 16: Now, if onMove is implemented, you can reorder with a long press even when not in edit mode, which allows these three actions to coexist. I've added custom drag-handles to make this behavior obvious to the user, but otherwise I'm going with exactly the solution given in the accepted answer, below.
I am not sure that the .environment is necessary in this case (I could be wrong). You can remove that piece.
Additionally, you should add an ID to each item in your foreach. This should ideally come from your model when you create new items (for example, your model can contain an ID variable = UUID()), but for the time being we can add it inline in your foreach.
I had to write some code on my end to get this up and running, so my solution is based on the code I spun up (very similar to yours, but missing your custom items object):
struct ReorderableListView: View {
var items: [X] // List of custom objects
var body: some View {
NavigationView {
List {
// added ID here
ForEach(items, id: \.self) { item in
NavigationLink(destination: CustomView(item)) { // winds up disabled by edit mode
Text(item.name)
}
.swipeActions(edge: .leading) { Button("Swipe") {print("swipe")} } // winds up disabled by edit mode
}
.onMove { from, to in
print("move \(from) to \(to)")
}
}
// REMOVED .environment(\.editMode, .constant(.active)) // always in edit mode for reordering
}
}
}
For reference, here is the code I wrote locally to fill in the gaps. This is what worked & what I implemented within your code:
struct ReorderableListView: View {
#State var items = [1, 2, 3] // List of custom objects
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: ContentView2()) { // winds up disabled by edit mode
Text("\(item)")
}
.swipeActions(edge: .leading) { Button("Swipe") {print("swipe")} } // winds up disabled by edit mode
}
.onMove { from, to in
print("move \(from) to \(to)")
}
}
// .environment(\.editMode, .constant(.active)) // always in edit mode for reordering
}
}
}

List view in SwiftUI takes up entire screen height

In SwiftUI, list view by default takes up entire height of the screen and pushes other elements/views to the bottom of the screen. But I want to append some elements/views where the list items exactly end.
You can add spacer() at appropriate places in the VStack or try something like below:
VStack {
CustomView1()
List {
Section(header: HeaderView(), footer: FooterView())
{
ForEach(viewModel.permissions) { permission in
CustomeView2()
}
GeneralView()//add the views at the end of list items
}
}.listStyle(GroupedListStyle())
}

Handling focus event changes on tvOS in SwiftUI

How do I respond to focus events on tvOS in SwiftUI?
I have the following SwiftUI view:
struct MyView: View {
var body: some View {
VStack {
Button(action: {
print("Button 1 pressed")
}) {
Text("Button 1")
}.focusable(true) { focused in
print("Button 1 focused: \(focused)")
}
Button(action: {
print("Button 2 pressed")
}) {
Text("Button 2")
}.focusable(true) { focused in
print("Button 2 focused: \(focused)")
}
}
}
Clicking either of the buttons prints out correctly. However, changing focus between the two buttons does not print anything.
This guy is doing the same thing with rows in a list & says it started working for him with the Xcode 11 GM, but I'm on 11.5 and it's definitely not working (at least not for Buttons (or Toggles - I tried those too)).
Reading the documentation, this appears to be the correct way to go about this, but it doesn't seem to actually work. Am I missing something, or is this just broken?
In case anybody else stumbles upon this question, the answer is to make your view data-focused
So, if you have a list of movies in a scrollview, you would add this to your outer view:
var myMovieList: [Movie];
#FocusState var selectedMovie: Int?;
And then somewhere in your body property:
ForEach(0..<myMovieList.count) { index in
MovieCard(myMovieList[i])
.focusable(true)
.focused($selectedMovie, equals: index)
}
.focusable() tells the OS that this element is focusable, and .focused tells it to make that view focused when the binding variable ($selected) equals the value passed to equals: ...
For example on tvOS, if you wrap this in a scrollview and press left/right, it will change the selection and update the selected index in the $selectedMovie variable; you can then use that to index into your movie list to display extra info.
Here is a more complete example that will also scale the selected view:
https://pastebin.com/jejwYxMU

SwiftUI List is not showing any items

I want to use NavigationView together with the ScrollView, but I am not seeing List items.
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView{
VStack {
Text("Some stuff 1")
List{
Text("one").padding()
Text("two").padding()
Text("three").padding()
}
Text("Some stuff 2")
}
}
}
}
}
All I see is the text. If I remove ScrollView I see it all, but the text is being pushed to the very bottom. I simply want to be able to add List and Views in a nice scrollable page.
The ScrollView expects dimension from content, but List expects dimension from container - as you see there is conflict, so size for list is undefined, and a result rendering engine just drop it to avoid disambiguty.
The solution is to define some size to List, depending of your needs, so ScrollView would now how to lay out it, so scroll view could scroll entire content and list could scroll internal content.
Eg.
struct ContentView: View {
#Environment(\.defaultMinListRowHeight) var minRowHeight
var body: some View {
NavigationView {
ScrollView{
VStack {
Text("Some stuff 1")
List {
Text("one").padding()
Text("two").padding()
Text("three").padding()
}.frame(minHeight: minRowHeight * 3).border(Color.red)
Text("Some stuff 2")
}
}
}
}
}
Just wanted to throw out an answer that fixed what I was seeing very similar to the original problem - I had put a Label() item ahead of my List{ ... } section, and when I deleted that Label() { } I was able to see my List content again. Possibly List is buggy with other items surrounding it (Xcode 13 Beta 5).