Reputation: 771
I'm trying to put a Text
for each visible (within the screen bounds) row in a List of their current index in all the visible rows.
I have an example here. If I have a list showing 3 rows at a time, and initially I want it showing like this:
___|___
A | 0
B | 1
C | 2
-------
And if I scroll the list down, and A goes out of the screen, then it will be like
___|___
B | 0
C | 1
D | 2
-------
Any idea how can I do it?
Besides, I used to use UITableView.visibleCells to obtain the visible cells on screen, but I couldn't find something similar in SwiftUI. How exactly am I suppose to get visible row in a list with only index provided in SwiftUI?
Hope someone can direct me to the right place. Thanks a lot!
Upvotes: 6
Views: 5554
Reputation: 955
I would suggest to use the onAppear modifier with LazyHStack or LazyVStack's subview. Place this LazyHStack or LazyVStack inside the ScrollView. e.g.
Here is the scroll view and LazyVStack
import SwiftUI
struct ProductListView: View {
var staff = [
Person(name: "Ram", phoneNumber: "(408) 555-4301"),
Person(name: "Aman", phoneNumber: "(919) 555-2481"),
Person(name: "John", phoneNumber: "(408) 555-4301"),
Person(name: "Jeff", phoneNumber: "(408) 555-4301"),
Person(name: "Boing", phoneNumber: "(408) 555-4301"),
Person(name: "Kim", phoneNumber: "(408) 555-4301"),
Person(name: "Juhi", phoneNumber: "(408) 555-4301"),
Person(name: "Mei Chen", phoneNumber: "(919) 555-2481"),
Person(name: "Rahul", phoneNumber: "(408) 555-4301"),
Person(name: "Mahesh", phoneNumber: "(408) 555-4301"),
Person(name: "Ninja", phoneNumber: "(408) 555-4301"),
Person(name: "Rohit", phoneNumber: "(408) 555-4301")
]
var body: some View {
ScrollView(.vertical) {
LazyVStack(alignment: .leading, spacing: 10) {
ForEach(staff) { person in
ProductListItem.init(title: person.name, description: person.phoneNumber)
.onAppear(perform: {
print("Item appred - ", person.name)
})
}
}
}
}
}
Here is the List Item
import SwiftUI
struct ProductListItem: View {
let title: String
let description: String
var body: some View {
ZStack(alignment: .bottom, content: {
Image("placeholder", bundle: nil)
.resizable()
HStack(alignment: .bottom, content: {
VStack(alignment: .leading, content: {
Text(title).font(.headline)
.foregroundStyle(.white)
Text(description).font(.subheadline)
.foregroundStyle(.white)
})
Spacer()
})
.padding()
.foregroundColor(.primary)
.background(Color.secondary
.colorInvert()
.opacity(0.90))
})
}
}
NOTE - Just to increase the item size add some placeholder image in assets folder.
Upvotes: 0
Reputation: 105
While you could use .onAppear
and .onDisappear
, those modifiers are based on SwiftUI's conception of appearance, rather than user visibility. It's common for a view, for example, to be out of sight for the user but still visible to SwiftUI until a new List
item is loaded.
To appropriately measure which List
items are visible to the user, you'll need the items to report their positions, then filter the ones based on some condition that determines visibility. In practice, that means using preferences to send information up the view hierarchy.
The best way to accomplish that is with anchor preferences (Swift with Majid has a very good article on this). If you're using a ScrollView
, you may be able to get away with scrollPosition(id:anchor:)
; but preferences allow you to specify your own filtering criteria.
First, you'll need the preference:
struct VisibleItem<Item> {
let item: Item
let anchor: Anchor<CGRect>
}
struct VisiblePreferenceKey<Item>: PreferenceKey {
typealias Value = [VisibleItem<Item>]
static var defaultValue: Value { [] }
static func reduce(value: inout Value, nextValue: () -> Value) {
value.append(contentsOf: nextValue())
}
}
VisibleItem
will be the struct useful for filtering via visibility. The reason it has an Anchor
is because it allows you to convert the geometry of a view into the coordinate space of another (in other words, it allows the filtering to actually encompass the user visible space)
The preference will then be applied to List
items that should report their positions:
struct UserImages: View {
let images: [UserImage]
var body: some View {
List(images) { image in
UserImageView(image: image)
.anchorPreference(key: VisiblePreferenceKey.self, value: .bounds) { [.init(item: image, anchor: $0)] }
}
}
}
Note that we're storing image
as the item so we can reference it after we filter.
Finally, on your List
, you can read the preference value and apply your filtering:
struct UserImages: View {
let images: [UserImage]
var body: some View {
List(images) { image in
// ...
}.backgroundPreferenceValue(VisiblePreferenceKey<UserImage>.self) { items in
GeometryReader { proxy in
let local = proxy.frame(in: .local)
let images = items
.filter { local.intersects(proxy[$0.anchor]) }
.map(\.item)
// Do something with `images`
}
}
}
}
I'm using backgroundPreferenceValue(_:_:)
here just to demonstrate, but as can be seen, the frame of the List
is stored in local
, and each item is filtered with respect to the List
's coordinate space (the proxy[$0.anchor]
), and checked to see if the two CGRect
s intersect (in other words, do they overlap?)
You could do a lot with this, but for your specific case, what you could do is use a Color.clear.onChange(of: images) {...}
and store the order of each item in your model so you can display it in your UI (items is already sorted by the preference key). If you wanted to be more strict, per say, and only include items that are completely visible (so a view that is half-scrolled out is not counted), you could e.g. change local.intersects(...)
to local.contains(...)
.
Upvotes: 1
Reputation: 550
You can use onAppear and onDisappear functions to maintain a list of visible rows and then use these to find a visible index.
struct ContentView: View {
let rows = (Unicode.Scalar("A").value...Unicode.Scalar("Z").value)
.map { String(Unicode.Scalar($0)!) }
@State var visibleRows: Set<String> = []
var body: some View {
List(rows, id: \.self) { row in
HStack {
Text(row)
.padding(40)
.onAppear { self.visibleRows.insert(row) }
.onDisappear { self.visibleRows.remove(row) }
}
Spacer()
Text(self.getVisibleIndex(for: row)?.description ?? "")
}
}
func getVisibleIndex(for row: String) -> Int? {
visibleRows.sorted().firstIndex(of: row)
}
}
Upvotes: 18