How can i return a loop? - swiftui

I want to know the scrollview offset and found in the internet that they did this in this way:
GeometryReader { geometry -> Text in
let newOffset = geometry.frame(in: .global).minY
if newOffset != self.offset {
self.offset = newOffset
}
return
Text("aha")
}
unfortunately my "return-type" is
ForEach (MathTaskType.allCases) { eachType in
MathTypeRow(mathType: eachType)
}
and i have no idea what to write here (xxxx).
i tried it with Group around it, but i didn't get this to run...
Thank you for your help.
GeometryReader { geometry -> xxxxxx in // what do i have to input here?
let newOffset = geometry.frame(in: .global).minY
if newOffset != self.offset {
self.offset = newOffset
}
return
ForEach (MathTaskType.allCases) { eachType in
MathTypeRow(mathType: eachType)
}
}

Here, the AnyView type is the rescue:
GeometryReader { geometry -> AnyView in
let newOffset = geometry.frame(in: .global).minY
if newOffset != self.offset {
self.offset = newOffset
}
return AnyView ( // <- Here!
ForEach (MathTaskType.allCases) { eachType in
MathTypeRow(mathType: eachType)
}
)
}
It converts any View to a universal type, without difficulties with generic types.
Of course, the ForEach also has a type from itself, I guess it's something like ForEach<[MathTaskType], MathTaskType.ID, MathTypeRow>. But that's not more beautiful, working with AnyView also gives flexibility for future changes.

return GeometryReader { geometry in
List(someData, id: \.self) { data in
Text("\(data)")
Text("x: \(geometry.frame(in: .global).origin.x)")
Text("y: \(geometry.frame(in: .global).origin.y)")
}
}
This would display the global (x,y coordinates inside your current view) for your reference.

Related

Scrollview conflicting with another scrollview. Swiftui

I have a scrollview who's content expands to fill the screen and stacks in front using the zIndex. The expanded content also has a scrollview for the content inside. I'm literally trying to mimic the Apps Stores "Today" tab with expanding cards and scrollable content inside.
The way I built this though I realized the expanding view is still part of the parent scrollview. As a result the scrollviews are nested and conflict. This was not what I intended.
Im very new to programming. This is the code that basically expands each card. Its pretty basic. A ternary expands the cards. The CardView is the cards content. Attached is a screenshot of what I'm trying to achieve. Need help here. Any info would be great. Been searching the internet for a way to do this right.
struct Media: View {
#EnvironmentObject var vm : ViewModel
#Environment(\.colorScheme) var mode
#State var showCard = false
#State var activeCard = -1
let height = UIScreen.main.bounds.height
var body: some View {
ZStack {
ScrollView (.vertical, showsIndicators: false) {
VStack (alignment:.center, spacing: 30) {
ForEach (vm.cards.indices, id: \.self) { i in
let z=vm.cards[i].z
GeometryReader { geom in
ZStack {
CardView (showCard: $showCard,
activeCard: self.$activeCard,
zValue: $vm.cards[i].z,
i: i,
cards: vm.cards[i])
}//Z
.frame(minHeight: showCard && activeCard == i ? height : nil) //Animates Card Scaling
.padding(.horizontal, showCard && activeCard == i ? 0:20) // Card Padding
.offset(y: i==activeCard ? -geom.frame(in: .global).minY : 0)
} //GEOM
.frame(minHeight: 450)
.zIndex(z)
} //LOOP
} //V
.padding([.bottom, .top])
} //SCROLLVIEW
} //Z
.background(Color("Background Gray"))
}
}
I don't see a problem with the nested ScrollViews, as you put the DetailView on top. I tried to rebuild a simplified version from your code, see below.
BTW: You don't need the outer ZStack.
And at some point you should consider using ForEach over the cards, not over the indices, otherwise you'll run into UI update problems when inserting or deleting cards. I left it for now to stay closer to your original.
struct ContentView: View {
#State var activeCard = -1
let height = UIScreen.main.bounds.height
var body: some View {
ScrollView (.vertical, showsIndicators: false) {
VStack (alignment:.center, spacing: 30) {
ForEach (data.indices, id: \.self) { i in
GeometryReader { geom in
ZStack {
DetailCellView(entry: data[i], isActive: activeCard == i)
.onTapGesture {
if activeCard == i {
activeCard = -1
} else {
activeCard = i
}
}
}//Z
.frame(minHeight: activeCard == i ? height : nil) //Animates Card Scaling
.padding(.horizontal, activeCard == i ? 0 : 20) // Card Padding
.offset(y: activeCard == i ? -geom.frame(in: .global).minY : 0)
} //GEOM
.frame(minHeight: 450)
.zIndex(activeCard == i ? 1 : 0)
.animation(.default, value: activeCard)
} //LOOP
} //V
// .padding([.bottom, .top])
} //SCROLLVIEW
.background(.gray)
}
}
for the while being: a custom outer dragview as workaround ...
struct ContentView: View {
#State var activeCard = -1
let height = UIScreen.main.bounds.height
#State private var offset = CGFloat.zero
#State private var drag = CGFloat.zero
var body: some View {
GeometryReader { geo in
VStack (alignment:.center, spacing: 30) {
ForEach (data.indices, id: \.self) { i in
GeometryReader { geom in
ZStack {
DetailCellView(entry: data[i],
isActive: activeCard == i)
.onTapGesture {
if activeCard == i {
activeCard = -1
} else {
activeCard = i
}
}
}
.frame(minHeight: activeCard == i ? height : nil) //Animates Card Scaling
.padding(.horizontal, activeCard == i ? 0 : 20) // Card Padding
.offset(y: activeCard == i ? -geom.frame(in: .global).minY : 0)
}
.frame(minHeight: 400)
.zIndex(activeCard == i ? 1 : 0)
.animation(.default, value: activeCard)
}
}
// custom drag view
.offset(x: 0 , y: offset + drag)
.background(.gray)
// drag
.gesture(DragGesture()
.onChanged({ value in
drag = value.translation.height
})
.onEnded({ value in
print(offset)
withAnimation(.easeOut) {
offset += value.predictedEndTranslation.height
offset = min(offset, 0)
offset = max(offset, -CGFloat(data.count * (400 + 30)) + height )
drag = 0
}
print(offset)
})
)
}
}
}
So I figured out how to accomplish what I was trying to accomplish.
struct ScrollingHelper: UIViewRepresentable {
let proxy: ScrollingProxy // reference type
func makeUIView(context: Context) -> UIView {
return UIView() // managed by SwiftUI, no overloads
}
func updateUIView(_ uiView: UIView, context: Context) {
proxy.catchScrollView(for: uiView) // here UIView is in view hierarchy
}
}
class ScrollingProxy {
private var scrollView: UIScrollView?
func catchScrollView(for view: UIView) {
if nil == scrollView {
scrollView = view.enclosingScrollView()
}
}
func disableScrolling(_ flag: Bool) {
scrollView?.isScrollEnabled = flag
print(flag)
}
}
extension UIView {
func enclosingScrollView() -> UIScrollView? {
var next: UIView? = self
repeat {
next = next?.superview
if let scrollview = next as? UIScrollView {
return scrollview
}
} while next != nil
return nil
}
}
I used the above code from https://stackoverflow.com/a/60855853/12299030 &
Disable Scrolling in SwiftUI List/Form
Then used in a TapGesture
scrollEnabled = false
scrollProxy.disableScrolling(scrollEnabled)
and used background modifier:
.background(ScrollingHelper(proxy: scrollProxy))

How to use geometry reader so that the view does not expand?

I have used geometry reader like this
GeometryReader { r in
ScrollView {
Text("SomeText").frame(width: r.size.width / 2)
}
}
The problem is that the reader expands vertically much like Spacer().
Is there anyway that I can make it not do this?
After googling around I found this answer here.
Create this new struct
struct SingleAxisGeometryReader<Content: View>: View {
private struct SizeKey: PreferenceKey {
static var defaultValue: CGFloat { 10 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
#State private var size: CGFloat = SizeKey.defaultValue
var axis: Axis = .horizontal
var alignment: Alignment = .center
let content: (CGFloat)->Content
var body: some View {
content(size)
.frame(maxWidth: axis == .horizontal ? .infinity : nil,
maxHeight: axis == .vertical ? .infinity : nil,
alignment: alignment)
.background(GeometryReader {
proxy in
Color.clear.preference(key: SizeKey.self, value: axis == .horizontal ? proxy.size.width : proxy.size.height)
}).onPreferenceChange(SizeKey.self) { size = $0 }
}
}
And then use it like this
SingleAxisGeometryReader { width in // For horizontal
// stuff here
}
or
SingleAxisGeometryReader(axis: .vertical) { height in // For vertical
// stuff here
}
With this answer, it’s now generic with no code change.
Since background is fit to actual view size always.you can use this trick, adding GeometryReader in background without changing the size of the view itself.
ScrollView {
}.background(
GeometryReader { r in
// stuff
}
)
}
It's somewhat unclear what you're actually trying to do with the views if it's not actually the code you gave at the top. With regards to that, though, you can swap the position of the GeometryReader and the ScrollView. What the GeometryReader does is find the frame of the available space, and it fills it. With a ScrollView the actual height is 0. So, this:
ScrollView {
GeometryReader {r in
Text("SomeText").frame(width: r.size.width / 2)
}
}

Programatically scroll to SwiftUI list position? [duplicate]

It looks like in current tools/system, just released Xcode 11.4 / iOS 13.4, there will be no SwiftUI-native support for "scroll-to" feature in List. So even if they, Apple, will provide it in next major released, I will need backward support for iOS 13.x.
So how would I do it in most simple & light way?
scroll List to end
scroll List to top
and others
(I don't like wrapping full UITableView infrastructure into UIViewRepresentable/UIViewControllerRepresentable as was proposed earlier on SO).
SWIFTUI 2.0
Here is possible alternate solution in Xcode 12 / iOS 14 (SwiftUI 2.0) that can be used in same scenario when controls for scrolling is outside of scrolling area (because SwiftUI2 ScrollViewReader can be used only inside ScrollView)
Note: Row content design is out of consideration scope
Tested with Xcode 12b / iOS 14
class ScrollToModel: ObservableObject {
enum Action {
case end
case top
}
#Published var direction: Action? = nil
}
struct ContentView: View {
#StateObject var vm = ScrollToModel()
let items = (0..<200).map { $0 }
var body: some View {
VStack {
HStack {
Button(action: { vm.direction = .top }) { // < here
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
Button(action: { vm.direction = .end }) { // << here
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
Divider()
ScrollViewReader { sp in
ScrollView {
LazyVStack {
ForEach(items, id: \.self) { item in
VStack(alignment: .leading) {
Text("Item \(item)").id(item)
Divider()
}.frame(maxWidth: .infinity).padding(.horizontal)
}
}.onReceive(vm.$direction) { action in
guard !items.isEmpty else { return }
withAnimation {
switch action {
case .top:
sp.scrollTo(items.first!, anchor: .top)
case .end:
sp.scrollTo(items.last!, anchor: .bottom)
default:
return
}
}
}
}
}
}
}
}
SWIFTUI 1.0+
Here is simplified variant of approach that works, looks appropriate, and takes a couple of screens code.
Tested with Xcode 11.2+ / iOS 13.2+ (also with Xcode 12b / iOS 14)
Demo of usage:
struct ContentView: View {
private let scrollingProxy = ListScrollingProxy() // proxy helper
var body: some View {
VStack {
HStack {
Button(action: { self.scrollingProxy.scrollTo(.top) }) { // < here
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
Button(action: { self.scrollingProxy.scrollTo(.end) }) { // << here
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
Divider()
List {
ForEach(0 ..< 200) { i in
Text("Item \(i)")
.background(
ListScrollingHelper(proxy: self.scrollingProxy) // injection
)
}
}
}
}
}
Solution:
Light view representable being injected into List gives access to UIKit's view hierarchy. As List reuses rows there are no more values then fit rows into screen.
struct ListScrollingHelper: UIViewRepresentable {
let proxy: ListScrollingProxy // reference type
func makeUIView(context: Context) -> UIView {
return UIView() // managed by SwiftUI, no overloads
}
func updateUIView(_ uiView: UIView, context: Context) {
proxy.catchScrollView(for: uiView) // here UIView is in view hierarchy
}
}
Simple proxy that finds enclosing UIScrollView (needed to do once) and then redirects needed "scroll-to" actions to that stored scrollview
class ListScrollingProxy {
enum Action {
case end
case top
case point(point: CGPoint) // << bonus !!
}
private var scrollView: UIScrollView?
func catchScrollView(for view: UIView) {
if nil == scrollView {
scrollView = view.enclosingScrollView()
}
}
func scrollTo(_ action: Action) {
if let scroller = scrollView {
var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
switch action {
case .end:
rect.origin.y = scroller.contentSize.height +
scroller.contentInset.bottom + scroller.contentInset.top - 1
case .point(let point):
rect.origin.y = point.y
default: {
// default goes to top
}()
}
scroller.scrollRectToVisible(rect, animated: true)
}
}
}
extension UIView {
func enclosingScrollView() -> UIScrollView? {
var next: UIView? = self
repeat {
next = next?.superview
if let scrollview = next as? UIScrollView {
return scrollview
}
} while next != nil
return nil
}
}
Just scroll to the id:
scrollView.scrollTo(ROW-ID)
Since SwiftUI structured designed Data-Driven, You should know all of your items IDs. So you can scroll to any id with ScrollViewReader from iOS 14 and with Xcode 12
struct ContentView: View {
let items = (1...100)
var body: some View {
ScrollViewReader { scrollProxy in
ScrollView {
ForEach(items, id: \.self) { Text("\($0)"); Divider() }
}
HStack {
Button("First!") { withAnimation { scrollProxy.scrollTo(items.first!) } }
Button("Any!") { withAnimation { scrollProxy.scrollTo(50) } }
Button("Last!") { withAnimation { scrollProxy.scrollTo(items.last!) } }
}
}
}
}
Note that ScrollViewReader should support all scrollable content, but now it only supports ScrollView
Preview
Preferred way
This answer is getting more attention, but I should state that the ScrollViewReader is the right way to do this. The introspect way is only if the reader/proxy doesn't work for you, because of a version restrictions.
ScrollViewReader { proxy in
ScrollView(.vertical) {
TopView().id("TopConstant")
...
MiddleView().id("MiddleConstant")
...
Button("Go to top") {
proxy.scrollTo("TopConstant", anchor: .top)
}
.id("BottomConstant")
}
.onAppear{
proxy.scrollTo("MiddleConstant")
}
.onChange(of: viewModel.someProperty) { _ in
proxy.scrollTo("BottomConstant")
}
}
The strings should be defined in one place, outside of the body property.
Legacy answer
Here is a simple solution that works on iOS13&14:
Using Introspect.
My case was for initial scroll position.
ScrollView(.vertical, showsIndicators: false, content: {
...
})
.introspectScrollView(customize: { scrollView in
scrollView.scrollRectToVisible(CGRect(x: 0, y: offset, width: 100, height: 300), animated: false)
})
If needed the height may be calculated from the screen size or the element itself.
This solution is for Vertical scroll. For horizontal you should specify x and leave y as 0
Thanks Asperi, great tip. I needed to have a List scroll up when new entries where added outside the view. Reworked to suit macOS.
I took the state/proxy variable to an environmental object and used this outside the view to force the scroll. I found I had to update it twice, the 2nd time with a .5sec delay to get the best result. The first update prevents the view from scrolling back to the top as the row is added. The 2nd update scrolls to the last row. I'm a novice and this is my first stackoverflow post :o
Updated for MacOS:
struct ListScrollingHelper: NSViewRepresentable {
let proxy: ListScrollingProxy // reference type
func makeNSView(context: Context) -> NSView {
return NSView() // managed by SwiftUI, no overloads
}
func updateNSView(_ nsView: NSView, context: Context) {
proxy.catchScrollView(for: nsView) // here NSView is in view hierarchy
}
}
class ListScrollingProxy {
//updated for mac osx
enum Action {
case end
case top
case point(point: CGPoint) // << bonus !!
}
private var scrollView: NSScrollView?
func catchScrollView(for view: NSView) {
//if nil == scrollView { //unB - seems to lose original view when list is emptied
scrollView = view.enclosingScrollView()
//}
}
func scrollTo(_ action: Action) {
if let scroller = scrollView {
var rect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
switch action {
case .end:
rect.origin.y = scroller.contentView.frame.minY
if let documentHeight = scroller.documentView?.frame.height {
rect.origin.y = documentHeight - scroller.contentSize.height
}
case .point(let point):
rect.origin.y = point.y
default: {
// default goes to top
}()
}
//tried animations without success :(
scroller.contentView.scroll(to: NSPoint(x: rect.minX, y: rect.minY))
scroller.reflectScrolledClipView(scroller.contentView)
}
}
}
extension NSView {
func enclosingScrollView() -> NSScrollView? {
var next: NSView? = self
repeat {
next = next?.superview
if let scrollview = next as? NSScrollView {
return scrollview
}
} while next != nil
return nil
}
}
my two cents for deleting and repositioning list at any point based on other logic.. i.e. after delete/update, for example going to top.
(this is a ultra-reduced sample, I used this code after network call back to reposition: after network call I change previousIndex )
struct ContentView: View {
#State private var previousIndex : Int? = nil
#State private var items = Array(0...100)
func removeRows(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
self.previousIndex = offsets.first
}
var body: some View {
ScrollViewReader { (proxy: ScrollViewProxy) in
List{
ForEach(items, id: \.self) { Text("\($0)")
}.onDelete(perform: removeRows)
}.onChange(of: previousIndex) { (e: Equatable) in
proxy.scrollTo(previousIndex!-4, anchor: .top)
//proxy.scrollTo(0, anchor: .top) // will display 1st cell
}
}
}
}
This can now be simplified with all new ScrollViewProxy in Xcode 12, like so:
struct ContentView: View {
let itemCount: Int = 100
var body: some View {
ScrollViewReader { value in
VStack {
Button("Scroll to top") {
value.scrollTo(0)
}
Button("Scroll to buttom") {
value.scrollTo(itemCount-1)
}
ScrollView {
LazyVStack {
ForEach(0 ..< itemCount) { i in
Text("Item \(i)")
.frame(height: 50)
.id(i)
}
}
}
}
}
}
}
MacOS 11: In case you need to scroll a list based on input outside the view hierarchy. I have followed the original scroll proxy pattern using the new scrollViewReader:
struct ScrollingHelperInjection: NSViewRepresentable {
let proxy: ScrollViewProxy
let helper: ScrollingHelper
func makeNSView(context: Context) -> NSView {
return NSView()
}
func updateNSView(_ nsView: NSView, context: Context) {
helper.catchProxy(for: proxy)
}
}
final class ScrollingHelper {
//updated for mac os v11
private var proxy: ScrollViewProxy?
func catchProxy(for proxy: ScrollViewProxy) {
self.proxy = proxy
}
func scrollTo(_ point: Int) {
if let scroller = proxy {
withAnimation() {
scroller.scrollTo(point)
}
} else {
//problem
}
}
}
Environmental object:
#Published var scrollingHelper = ScrollingHelper()
In the view: ScrollViewReader { reader in .....
Injection in the view:
.background(ScrollingHelperInjection(proxy: reader, helper: scrollingHelper)
Usage outside the view hierarchy: scrollingHelper.scrollTo(3)
As mentioned in #lachezar-todorov's answer Introspect is a nice library to access UIKit elements in SwiftUI. But be aware that the block you use for accessing UIKit elements are being called multiple times. This can really mess up your app state. In my cas CPU usage was going %100 and app was getting unresponsive. I had to use some pre conditions to avoid it.
ScrollView() {
...
}.introspectScrollView { scrollView in
if aPreCondition {
//Your scrolling logic
}
}
Another cool way is to just use namespace wrappers:
A dynamic property type that allows access to a namespace defined by the persistent identity of the object containing the property (e.g. a view).
struct ContentView: View {
#Namespace private var topID
#Namespace private var bottomID
let items = (0..<100).map { $0 }
var body: some View {
ScrollView {
ScrollViewReader { proxy in
Section {
LazyVStack {
ForEach(items.indices, id: \.self) { index in
Text("Item \(items[index])")
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.green.cornerRadius(16))
}
}
} header: {
HStack {
Text("header")
Spacer()
Button(action: {
withAnimation {
proxy.scrollTo(bottomID)
}
}
) {
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
.padding(.vertical)
.id(topID)
} footer: {
HStack {
Text("Footer")
Spacer()
Button(action: {
withAnimation {
proxy.scrollTo(topID) }
}
) {
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
}
.padding(.vertical)
.id(bottomID)
}
.padding()
}
}
.foregroundColor(.white)
.background(.black)
}
}
Two parts:
Wrap the List (or ScrollView) with ScrollViewReader
Use the scrollViewProxy (that comes from ScrollViewReader) to scroll to an id of an element in the List. You can seemingly use EmptyView().
The example below uses a notification for simplicity (use a function if you can instead!).
ScrollViewReader { scrollViewProxy in
List {
EmptyView().id("top")
}
.onReceive(NotificationCenter.default.publisher(for: .ScrollToTop)) { _ in
// when using an anchor of `.top`, it failed to go all the way to the top
// so here we add an extra -50 so it goes to the top
scrollViewProxy.scrollTo("top", anchor: UnitPoint(x: 0, y: -50))
}
}
extension Notification.Name {
static let ScrollToTop = Notification.Name("ScrollToTop")
}
NotificationCenter.default.post(name: .ScrollToTop, object: nil)

why is VStack not working in GeometryReader with scrollview?

My scrollview with vStack worked (in AppleTV - i did not test in iOS) without GeometryReader.
Then i added the geometryreader and the VStack "collapses" like a ZStack.
What can i do to solve this? (and yes, i need the geometryreader ;))
struct ContentView: View {
#State var offset : CGFloat = 0
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
GeometryReader { geometry -> AnyView in
let newOffset = geometry.frame(in: .global).minY
if newOffset != self.offset {
self.offset = newOffset
print("new offset",newOffset)
}
return AnyView (
ForEach (0..<5) { _ in
Text("umpf")
}
)
}
}
}
}
}
Result:
and this code works as desired:
struct ContentView: View {
#State var offset : CGFloat = 0
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
// GeometryReader { geometry -> AnyView in
// let newOffset = geometry.frame(in: .global).minY
// if newOffset != self.offset {
// self.offset = newOffset
// print("new offset",newOffset)
// }
// return AnyView (
ForEach (0..<5) { _ in
Text("umpf")
}
// )
// }
}
}
}
}
result:
Let me suppose that initially you wanted this (and it does not break ScrollView layout & scrolling)
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
ForEach (0..<5) { _ in
Text("umpf")
}
}.background(
GeometryReader { geometry -> Color in
let newOffset = geometry.frame(in: .global).minY
if newOffset != self.offset {
self.offset = newOffset
print("new offset",newOffset)
}
return Color.clear
}
)
}
}

Get onAppear behaviour from list in ScrollView in SwiftUI

When creating a List view onAppear triggers for elements in that list the way you would expect: As soon as you scroll to that element the onAppear triggers. However, I'm trying to implement a horizontal list like this
ScrollView(.horizontal) {
HStack(spacing: mySpacing) {
ForEach(items) { item in
MyView(item: item)
.onAppear { \\do something }
}
}
}
Using this method the onAppear triggers for all items at once, that is to say: immediately, but I want the same behavior as for a List view. How would I go about doing this? Is there a manual way to trigger onAppear, or control when views load?
Why I want to achieve this: I have made a custom Image view that loads an image from an URL only when it appears (and substitutes a placeholder in the mean time), this works fine for a List view, but I'd like it to also work for my horizontal 'list'.
As per SwiftUI 2.0 (XCode 12 beta 1) this is finally natively solved:
In a LazyHStack (or any other grid or stack with the Lazy prefix) elements will only initialise (and therefore trigger onAppear) when they appear on screen.
Here is possible approach how to do this (tested/worked with Xcode 11.2 / iOS 13.2)
Demo: (just show dynamically first & last visible cell in scrollview)
A couple of important View extensions
extension View {
func rectReader(_ binding: Binding<CGRect>, in space: CoordinateSpace) -> some View {
self.background(GeometryReader { (geometry) -> AnyView in
let rect = geometry.frame(in: space)
DispatchQueue.main.async {
binding.wrappedValue = rect
}
return AnyView(Rectangle().fill(Color.clear))
})
}
}
extension View {
func ifVisible(in rect: CGRect, in space: CoordinateSpace, execute: #escaping (CGRect) -> Void) -> some View {
self.background(GeometryReader { (geometry) -> AnyView in
let frame = geometry.frame(in: space)
if frame.intersects(rect) {
execute(frame)
}
return AnyView(Rectangle().fill(Color.clear))
})
}
}
And a demo view of how to use them with cell views being in scroll view
struct TestScrollViewOnVisible: View {
#State private var firstVisible: Int = 0
#State private var lastVisible: Int = 0
#State private var visibleRect: CGRect = .zero
var body: some View {
VStack {
HStack {
Text("<< \(firstVisible)")
Spacer()
Text("\(lastVisible) >> ")
}
Divider()
band()
}
}
func band() -> some View {
ScrollView(.horizontal) {
HStack(spacing: 10) {
ForEach(0..<50) { i in
self.cell(for: i)
.ifVisible(in: self.visibleRect, in: .named("my")) { rect in
print(">> become visible [\(i)]")
// do anything needed with visible rects, below is simple example
// (w/o taking into account spacing)
if rect.minX <= self.visibleRect.minX && self.firstVisible != i {
DispatchQueue.main.async {
self.firstVisible = i
}
} else
if rect.maxX >= self.visibleRect.maxX && self.lastVisible != i {
DispatchQueue.main.async {
self.lastVisible = i
}
}
}
}
}
}
.coordinateSpace(name: "my")
.rectReader(self.$visibleRect, in: .named("my"))
}
func cell(for idx: Int) -> some View {
RoundedRectangle(cornerRadius: 10)
.fill(Color.yellow)
.frame(width: 80, height: 60)
.overlay(Text("\(idx)"))
}
}
I believe what you want to achieve can be done with LazyHStack.
ScrollView {
LazyVStack {
ForEach(1...100, id: \.self) { value in
Text("Row \(value)")
.onAppear {
// Write your code for onAppear here.
}
}
}
}