SwiftUI not maintaining horizontal scroll position in nested list - ios

I've been looking into this after solving this issue on my Android App using savedInstanceState and some custom adapter work. I have a nested view (vertical with horizontal scrollviews contained within) and noticed while scrolling that the horizontal lists will reset to an index of 0 once the view is off the screen and more than likely recycled.
I have looked into ScrollViewReader but could not figure out how to listen to the currently viewable index range ( in order to reset it later ). The other option seems to be using GeometryReader to do some view calculation, but that seems a bit off while using LazyGrids.
An example of the view that I am describing:
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .top){
VStack {
HStack {
// header stuff
}
ScrollView(showsIndicators: false) {
// Vertical list
LazyVGrid(columns: gridItemLayout, spacing: 17) {
ForEach(parentData, id: \.key) { parent in
Text(parent.title)
ZStack {
Image("image")
ScrollViewReader { scrollView in
ScrollView(.horizontal, showsIndicators: false) {
// Horizontal lists
LazyHGrid(rows: gridItemLayout, spacing: 20){
ForEach(data, id: \.key) { data in
Button(action: {
//action
}) {
FeaturedCellView(data: data).equatable()
}
}
}.fixedSize(horizontal: true, vertical: false)
}
}
}
}.fixedSize(horizontal: false, vertical: true)
}
}
}
}
}
}
Examples of the views saving states can be found in apps like Netflix, or even in the App Store's horizontal grids.
I was hoping someone had solved this before and if so what you did.
Thanks for your help!

Related

How to prevent covering a ScrollView embedded in ZStack when scrolling to the bottom of the ScrollView?

I have HStack and ScrollView in ZStack, how can I prevent the HStack from covering the bottom of ScrollView? I think there is a modifier to fix this issue, but I can’t remember it.
struct Messages: View {
var body: some View {
ZStack(alignment: .bottom) {
ScrollView {
VStack() {
MessagesList()
}
}
MessageInput()
}
}
}
You can drop the ZStack and declare the view that sticks to the bottom using the .safeAreaInset modifier, e.g.:
ScrollView {
// etc.
}
.safeAreaInset(edge: .bottom) {
MessageInput()
}
More information on Hacking with Swift and Apple’s developer docs

Center Scroll View content

I have centered my Scroll View by using GeometryReader like this:
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { geom in
ScrollView(.vertical, showsIndicators: false) {
ZStack {
my_super_view()
}
.navigationBarTitle("Main")
.frame(width: geom.size.width)
.frame(minHeight: geom.size.height)
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
And it works almost great. My problem is that the height of container ZStack is too big and the page could be scrolled down like this:
And when i try to do smth like:
.frame(minHeight: geom.size.height-52)
The page stops scrolling, but is poorly centered. How can I properly center the page so that it fits entirely on the main screen without scrolling below it?
UPD:
I found out that i could use position() instead of frame(). In that case page centred properly, but it may cause some issues in future.
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { geom in
ScrollView(.vertical, showsIndicators: false) {
ZStack {
my_super_view()
}
.navigationBarTitle("Main")
.position(x: geom.size.width/2, y: geom.size.height/2)
}
}
.navigationBarTitle("Диа Компаньон")
.navigationBarTitleDisplayMode(.inline)
}.navigationViewStyle(StackNavigationViewStyle())
}
}
I had to use .inline NavigationBarDisplayMode and position instead of setting up width and height of ZStack

How to align ScrollView content to top when both axes are used?

Here's a simple example:
struct Example: View {
var body: some View {
ScrollView([.horizontal, .vertical], showsIndicators: false, content: {
LazyVStack(content: {
ForEach(1...10, id: \.self) { count in
Text("Item \(count)")
}
})
})
}
}
The problem is that when both axes are used ([.horizontal, .vertical]), ScrollView automatically centers any content inside vertically and horizontally. I have a big data table in the ScrollView, and I need it to be aligned to top instead but I couldn't figure out how to do this. Usual stuff with Vstacks and Spacers doesn't work here at all.
I made an example, with a Slider so you can interactively test it.
This works by making sure the content within the ScrollView is at least as high as the ScrollView itself. This leads to the content filling the whole vertical space, so it will start at the top.
Code:
struct Example: View {
#State private var count: Double = 10
var body: some View {
VStack {
GeometryReader { geo in
ScrollView([.horizontal, .vertical], showsIndicators: false, content: {
VStack(spacing: 0) {
LazyVStack {
ForEach(1 ... Int(count), id: \.self) { count in
Text("Item \(count)")
}
}
Spacer(minLength: 0)
}
.frame(width: geo.size.width)
.frame(minHeight: geo.size.height)
})
}
Slider(value: $count, in: 10 ... 100)
}
}
}
In some cases you may need to use .frame(minWidth: geo.size.width) rather than just width. This can be in the same line as the minHeight.

SwiftUI HStack fill whole width with equal spacing

I have an HStack:
struct BottomList: View {
var body: some View {
HStack() {
ForEach(navData) { item in
NavItem(image: item.icon, title: item.title)
}
}
}
}
How do I perfectly center its content with equal spacing automatically filling the whole width?
FYI just like Bootstraps CSS class .justify-content-around
The frame layout modifier, with .infinity for the maxWidth parameter can be used to achieve this, without the need for an additional Shape View.
struct ContentView: View {
var data = ["View", "V", "View Long"]
var body: some View {
VStack {
// This will be as small as possible to fit the data
HStack {
ForEach(data, id: \.self) { item in
Text(item)
.border(Color.red)
}
}
// The frame modifier allows the view to expand horizontally
HStack {
ForEach(data, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity)
.border(Color.red)
}
}
}
}
}
The various *Stack types will try to shrink to the smallest size possible to contain their child views. If the child view has an ideal size, then the *Stack will not expand to fill the screen. This can be overcome by placing each child on top of a clear Rectangle in a ZStack, because a Shape will expand as much as possible. A convenient way to do this is via an extension on View:
extension View {
func inExpandingRectangle() -> some View {
ZStack {
Rectangle()
.fill(Color.clear)
self
}
}
}
You can then call it like this:
struct ContentView: View {
var data = ["View", "View", "View"]
var body: some View {
VStack {
// This will be as small as possible to fit the items
HStack {
ForEach(data, id: \.self) { item in
Text(item)
.border(Color.red)
}
}
// Each item's invisible Rectangle forces it to expand
// The .fixedSize modifier prevents expansion in the vertical direction
HStack {
ForEach(data, id: \.self) { item in
Text(item)
.inExpandingRectangle()
.fixedSize(horizontal: false, vertical: true)
.border(Color.red)
}
}
}
}
}
You can adjust the spacing on the HStack as desired.
I inserted Spacer() after each item...but for the LAST item, do NOT add a Spacer():
struct BottomList: View {
var body: some View {
HStack() {
ForEach(data) { item in
Item(title: item.title)
if item != data.last { // match everything but the last
Spacer()
}
}
}
}
}
Example list that is evenly spaced out even when item widths are different:
(Note: The accepted answers .frame(maxWidth: .infinity) did not work for all cases: it did not work for me when it came to items that have different widths)
If items are fullwidth compatible, it will be done automatically, you can wrap items between spacers to make it happen:
struct Resizable: View {
let text: String
var body: some View {
HStack {
Spacer()
Text(text)
Spacer()
}
}
}
So you. can use it in a loop like:
HStack {
ForEach(data, id: \.self) { item in
Resizable(text: item)
}
}
You can also use spacing in stacks ... ie
HStack(spacing: 30){
Image("NetflixLogo")
.resizable()
.scaledToFit()
.frame(width: 40)
Text("TV Show")
Text("Movies")
Text("My List")
}
.frame(maxWidth: .infinity)
output result looks like this ...
If your array has repeating values, use array.indices to omit a spacer after the last element.
HStack() {
ForEach(data.indices) { i in
Text("\(data[i])")
if i != data.last {
Spacer()
}
}
}

How to implement a horizontal table view?

I'm trying to implement a horizontal table view in swift with the following functionalities:
Images listed in a horizontal manner
Horizontal scrolling left and right
See wireframe attached.
Can someone point me in the right direction? I tried digging into UICollectionView to implement this.
- SwiftUI
This question is asked a lot and there are so many answers out there for using CollectionView. But in SwiftUI you can use a horizontal ScrollView containing a Horizontal Stack like this:
struct CardView: View {
var body: some View {
Rectangle()
.foregroundColor(.gray)
.frame(width: 100, height: 80, alignment: .center)
.cornerRadius(8)
}
}
struct ContentView: View {
var body: some View {
VStack {
HStack {
Text("Categories")
Spacer()
Button(action: {}) { Text("More") }
}.padding(16)
ScrollView(.horizontal) {
HStack(spacing: 16) {
ForEach(0...15, id: \.self) { _ in
CardView()
}
}.padding(16)
}
}
}
}
- Result
Note that you can use this inside a UITableViewCell later this month when SwiftUI officially released by Apple.

Resources