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)
}
}
}
Related
Context: I need to pin a view in a ScrollView to the top of the screen when scrolling, so I use a LazyVStack with pinnedViews, set the view I need as Section. All good.
Issue: The ScrollView other views might change the content while the view is scrolled to the bottom, when that happens the screen removes all views and doesn't display them back unless I scroll to the top.
Question: Is there another way to pin a view to the top? (I tried to use List, but not exactly what I need) Or is possible to make a custom Stack with pinned views?
This is my answer for another post, but it seems like you both are having a similar problem. Download and import TrackableScrollView, and try out the below code. While scrolling, there is a pinned View() which is displayed at the top of the screen.
Link package: https://github.com/maxnatchanon/trackable-scroll-view
Code:
import SwiftUI
import SwiftUITrackableScrollView //Added
import Combine
struct GameTabView: View {
#State private var scrollViewContentOffset = CGFloat(0) //Added
#State var selectedTab: Int = 0
init() {
UITableView.appearance().sectionHeaderTopPadding = 0
}
var body: some View {
listView
.ignoresSafeArea()
}
var listView: some View {
ZStack { //Added
TrackableScrollView(.vertical, showIndicators: true, contentOffset: $scrollViewContentOffset) {
VStack {
Color.gray.frame(height: 400)
sectionView
}
}
if(scrollViewContentOffset > 400) {
VStack {
headerView
Spacer()
}
}
}
}
var sectionView: some View {
Section {
tabContentView
.transition(.scale) // FIXED
.background(Color.blue)
} header: {
headerView
}
}
private var headerView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
Button {
withAnimation {
selectedTab = 0
}
} label: {
Text("AAAA")
.padding()
}
Button {
withAnimation {
selectedTab = 1
}
} label: {
Text("BBBB")
.padding()
}
Button {
withAnimation {
selectedTab = 2
}
} label: {
Text("BBBB")
.padding()
}
}
}
}
.background(Color.green)
}
#ViewBuilder private var tabContentView: some View {
switch selectedTab {
case 0:
DummyScreen(title: "FIRST", color: .red)
case 1:
DummyScreen(title: "SECOND", color: .green)
case 2:
DummyScreen(title: "THIRD", color: .blue)
default:
EmptyView()
}
}
}
struct DummyScreen: View {
let title: String
let color: Color
var body: some View {
VStack {
ForEach(0..<15, id: \.self) { index in
HStack {
Text("#\(index): title \(title)")
.foregroundColor(Color.black)
.font(.system(size: 30))
.padding(.vertical, 20)
Spacer()
}
.background(Color.yellow)
}
}
.background(color)
}
}
From personal experience, I have done this using a VStack.
VStack{
PinnedItems()
LazyVStack {
OtherItems()
}
}
Then Pinned items will not scroll and are permanently attached to the top. And the lazyVStack will still be able to scroll underneath.
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.
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):
I am working through a sample app to familiarize myself with Swift and SwiftUI and am wanting to combine the ideas of composite layouts using custom views and GeometryReader to determine the screen-size of the device.
The page I'm trying to build is a scrollable list of friends where each row item is a button that can be selected to expand the list to see more details of each friend. I've built the button and all containing views as a FriendView to use inside of my FriendListView. I have defined 2 GeometryReader views within the FriendListView but do not know how I can define the size of each row from within the FriendView to make it maintain the appropriate size.
The need for this comes from not every image that could be set by a user later on for their friend will have the same dimensions so I want to keep it consistent. I understand I could set a .frame() modifier and hardcode the width but I would like it to be relational to the screen to be consistent across all phone screens.
Example: When a row is not expanded, the image should take up half of the device width. When it is expanded I would want it to take up one quarter of the device screen width.
Screenshot
Application View
Sample Code
struct FriendListView: View {
var pets = ["Woofer", "Floofer", "Booper"]
var body: some View {
GeometryReader { geo1 in
NavigationView {
GeometryReader { geo2 in
List(self.pets, id: \.self) { pet in
FriendView(name: pet)
}// End of List
}
.navigationBarTitle(Text("Furiends"))
}// End of NavigationView
}// End of GeometryReader geo1
}// End of body
}
struct FriendView: View {
#State private var isExpanded = false
var randomPic = Int.random(in: 1...2)
var name = ""
var breed = "Dawg"
var body: some View {
Button(action: self.toggleExpand) {
HStack {
VStack {
Image("dog\(self.randomPic)")
.resizable()
.renderingMode(.original)
.scaledToFill()
.clipShape(Circle())
if self.isExpanded == false {
Text(self.name)
.font(.title)
.foregroundColor(.primary)
}
}
if self.isExpanded == true {
VStack {
Text(self.name).font(.title)
Text(self.breed).font(.headline)
}
.foregroundColor(.primary)
}
}
}// End of Button
}
func toggleExpand() {
isExpanded.toggle()
}
}
You can define a GeometryReader in the top level FriendListView and pass the screen width to the FriendView:
struct FriendListView: View {
var pets = ["Woofer", "Floofer", "Booper"]
var body: some View {
GeometryReader { geo in
NavigationView {
List(self.pets, id: \.self) { pet in
FriendView(name: pet, screenWidth: geo.size.width)
}
.navigationBarTitle(Text("Furiends"))
}
}
}
}
Then use the .frame to set the maximum width for an image.
struct FriendView: View {
...
var screenWidth: CGFloat
var imageWidth: CGFloat {
isExpanded ? screenWidth / 4 : screenWidth / 2
}
var body: some View {
Button(action: self.toggleExpand) {
HStack {
VStack {
Image("dog\(self.randomPic)")
.resizable()
.renderingMode(.original)
.scaledToFill()
.clipShape(Circle())
.frame(width: imageWidth, height: imageWidth) // <- add frame boundaries
if self.isExpanded == false {
Text(self.name)
.font(.title)
.foregroundColor(.primary)
}
}
if self.isExpanded == true {
VStack {
Text(self.name).font(.title)
Text(self.breed).font(.headline)
}
.foregroundColor(.primary)
}
}
}
}
func toggleExpand() {
isExpanded.toggle()
}
}
In case you'd want your picture to be centered you can use Spacer:
HStack {
Spacer()
FriendView(name: pet, screenWidth: geo.size.width)
Spacer()
}
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()
}
}
}