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

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.

Related

How to fill SwiftUI's VStack from bottom to top with items

I have been searching but could not find a way.
I would like to fill VStack view from bottom to top.
When a Text view added as a child to VStack for example it is places at the top.
I wanted to add it to the bottom of screen and second one on top of the first one.
#State var items: [Int] = [0]
var body: some View {
HStack {
Button(action: {
items.append( items.count + 1)
}){
Text("Add")
}
ScrollView() {
ForEach(0..<items.count, id: \.self) { index in
VStack(spacing:5) {
Text(String(items[index]))
}
}
}
}
}
What you get on top of screen;
1
2
What I want at the bottom of screen;
2
1
You can achieve it by applying rotationEffect to your ScrollView and its inner content.
#State var items: [Int] = [0]
var body: some View {
HStack {
Button(action: {
//Insert Items at 0 position to add content from bottom to top side
items.insert(items.count, at: 0)
}){
Text("Add")
}
ScrollView(showsIndicators: false) {
ForEach(0..<items.count, id: \.self) { index in
Text(String(items[index])).padding(.vertical,5)
}.rotationEffect(Angle(degrees: 180)) //After rotating scrollview, now rotate your inner content to upright position.
}.rotationEffect(Angle(degrees: -180)) //Rotate scrollview to bottom side
}
}
How's this?
ScrollView {
VStack(spacing: 5) {
Spacer()
ForEach(items.map(String.init).reversed(), id: \.self) { item in
Text(item)
}
}
}

SwiftUI not maintaining horizontal scroll position in nested list

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!

SwiftUI ScrollView programmatically scroll to top based on change of dropdown

I have the following code:
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(1...7, id: \.self) { num in
Text("\(num)")
.font(.title3)
.padding([.top, .bottom], 5)
.onTapGesture {
self.getGames()
}
}
}
}
ScrollView() {
GameCell(games: $games, picks: self.$playerPicks)
}
.onAppear(perform: getGames)
}
}
It gets all the games and all, based on the value of num; as it should based on .onAppear(perform: getGames)
When num is selected it refreshes the main ScrollVeiw with the GameCell but if the main ScrollView (with the GameCells) is scrolled to any position, when the data refreshes it stays at that location... is there a way to have it scroll to the top of the main ScrollView (with the GameCells)?
UPDATE
I updated the code above, there are two scrollviews, I would like to have the second scrollveiw which has the GameCells scroll to the top each time the new value is selected in the first scrollview.
You can use ScrollViewReader (docs & HWS article).
For your use case, we need to save the ScrollViewProxy in a #State variable so it can be accessed outside of the closure. Use it like so:
struct ContentView: View {
#State private var reader: ScrollViewProxy?
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(1 ... 7, id: \.self) { num in
Text("\(num)")
.font(.title3)
.padding([.top, .bottom], 5)
.onTapGesture {
self.getGames()
print("Tap:", num)
// Scroll to tag 'GameCell-top'
reader?.scrollTo("GameCell-top", anchor: .top)
}
}
}
}
ScrollView {
ScrollViewReader { reader in
LinearGradient(
gradient: Gradient(colors: [.red, .blue]),
startPoint: .top,
endPoint: .bottom
)
.frame(height: 1000)
.id("GameCell-top") // <- ID so reader works
.tag("GameCell-top") // <- Tag view for reader
.onAppear {
self.reader = reader // <- Set current reader proxy
}
// You can uncomment this view and do a similar thing to the VStack above
// GameCell(games: $games, picks: self.$playerPicks)
}
}
.onAppear(perform: getGames)
}
}
}
Result (I scroll bottom scroll view, then tap any number from 1 to 7 at the top):

Custom ScrollView Indicator in SwiftUI

Is it possible to create a custom horizontal indicator that has empty and filled circles to show how many images there are and the current position?
The below attempt uses a lazyHStack and OnAppear but, judging from the console output, it doesn't work properly since scrolling back and forth doesn't recall the onAppear consistently.
import SwiftUI
struct ContentView: View {
let horizontalScrollItems = ["wind", "hare.fill", "tortoise.fill", "rosette" ]
var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(horizontalScrollItems, id: \.self) { symbol in
Image(systemName: symbol)
.font(.system(size: 200))
.frame(width: geometry.size.width)
.onAppear(){print("\(symbol)")}
}
}
}
}
}
}
This is the desired indicator. I'm just not sure how to properly fill and empty each circle as the user scrolls back and forth. Appreciate the help!
You can get the desired result using TabView() and PageTabViewStyle()
Note : This will work from SwiftUI 2.0
Here is the code :
struct ContentView: View {
let horizontalScrollItems = ["wind", "hare.fill", "tortoise.fill", "rosette" ]
var body: some View {
GeometryReader { geometry in
TabView(){
ForEach(horizontalScrollItems, id: \.self) { symbol in
Image(systemName: symbol)
.font(.system(size: 200))
.frame(width: geometry.size.width)
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
Result :

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()
}
}
}

Resources