Underline content of a VStack - swiftui

I’m trying to add an line at the bottom of a VStack that fills to width of the VStack which is determined by the other content in the VStack, but the Rectangle I am using fills up the available space of the entire view.
struct ContentView: View {
var body: some View {
VStack {
Text("Testing123")
Rectangle().frame(height: 2)
}
}
}
How can I make the Rectangle only have the width necessary for the VStack to fit its content?

Here is one way to do it.
Put your Rectangle in an .overlay() of the VStack. Put the rectangle in its own VStack and use a Spacer to push the Rectangle to the bottom. Control the spacing between the rectangle and your original VStack by adding .padding to the last view in the VStack.
struct ContentView: View {
var body: some View {
VStack {
Text("Hi")
Text("Testing123")
Text("Bye").padding(.bottom, 10)
}
.overlay(
VStack {
Spacer()
Rectangle().frame(height: 2)
}
)
}
}

Related

Button Text Out-Of-Sync with Tappable Area

I have a navigation view with tab views inside. The reason for this arrangement is to make the tab views disappear when within the navigation views.
I am trying to place a button at the top right corner of the screen (outside the safe area). If EntryView is called directly, the view correctly places the button ("Save") in the corner without use of offset() or position() view modifiers.
When the tab view is called before EntryView, the Save button appears an inch or so below where I would like it to appear. With offset() or position() I can move the button text back where I would like it to appear, but the tappable area doesn't move to the new location.
I have tried different arrangements of ZStack and VStack, but the arrangement below is the closest I have come to getting the button to work in the upper right corner.
Here is where I would like the button to appear: https://i.stack.imgur.com/AMdKQ.png
Is there any way to move the tappable area up to where the text is located?
Or is there some better way to draw the top part of the view?
This code can be dropped directly into Xcode for analysis.
struct ContentView: View {
#State private var selectedTab: Tabs = .home
var body: some View {
NavigationView {
TabView (selection: $selectedTab) {
EntryView()
.tabItem {
Label("Home", systemImage: "house.circle.fill")
}.tag(Tabs.home)
HistoryView()
.tabItem {
Label("History", systemImage: "clock.fill")
}.tag(Tabs.history)
}
}
}
}
struct EntryView: View {
var body: some View {
GeometryReader { g in
ZStack (alignment: .top) {
Color.blue
.frame(height: (g.safeAreaInsets.top) * 0.6, alignment: .top)
.ignoresSafeArea()
HStack {
Spacer()
Button(action: {
print("Save tapped!")
}) {
Text("Save")
.font(Font.title3.bold())
.foregroundColor(.red)
.offset(y: g.size.height * -0.14)
// .position(x: g.size.width * 0.90, y: g.size.height * -0.115)
}
}
.padding(.trailing, 10)
}
}
}
}
struct HistoryView: View {
var body: some View {
VStack (alignment: .leading) {
Text("History Tab")
.padding(.top)
}
}
}
enum Tabs: String {
case home
case history
}

Scrollview doesn't scroll past views with an offset

To solve a much more complicated problem, I created the following simple test project:
struct ContentView: View {
var body: some View {
ScrollView {
ZStack {
ForEach(0..<50) { index in
Text("Test \(index)")
.offset(x: 0, y: CGFloat(index * 20))
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This draws 50 Text views inside a ZStack, each one with a larger y offset, so that they are drawn down past the visible part of the screen:
The whole thing is wrapped inside of a ScrollView, so I expect that I should be able to scroll down to the last view.
However, it doesn't scroll past Test 26.
How can I arrange views inside of a ZStack by assigning offsets and update the ScrollView's contentSize?
The content size is calculated by the size of the view inside the ScrollView. So that only thing we can do is to change that view size.
By default VStack size is the total size (actual view sizes) of views inside it.
As well as we can use frame() to change the size in this case. check apple document
Since VStack arrange view in the middle we can align it to the .top.
struct ContentView: View {
var body: some View {
ScrollView() {
VStack {
ForEach(0..<50) { index in
Text("Test \(index)")
.offset(x: 0, y: CGFloat(index * 20))
}
}
.frame( height: 2000, alignment: .top) //<=here
.border(Color.black)
}
}
}
If I understood your intention correctly then you don't need offset in this scenario at all (SwiftUI works differently)
ScrollView {
VStack {
ForEach(0..<50) { index in
Text("Test \(index)") // << use .padding if needed
}
}
}
Note: The offset does not change view layout, view remains in the same place where it was created, but rendered in place of offset; frames of other views also not affected. Just be aware.

SwiftUI: Alignment not applied

I have a small example that U do not understand:
Why is the alignment of my ZStack not applied to all of it's children? The TopView stays on top but I would expect, that every child would be on bottom right:
struct ContentView: View {
var body: some View {
ZStack(alignment: .bottomTrailing) {
VStack {
TopView()
Spacer()
}
Text("A new layer")
}.padding()
}
}
struct TopView: View {
var body: some View {
HStack(alignment: .top) {
VStack {
Text("SwiftUI")
Text("Layout")
}
Spacer()
Image(systemName: "star")
}
}
}
By default all the views are in the middle of area they need. Even you're using VStack it will be in the middle of the screen and be with height of two Text (as in your example).
ZStack has the same behavior - it will be as small as possible and right in the center of safe area.
Spacer just under TopView tries to take all the free space. Just as Spacer between star and texts.
So VStack in your ZStack technically is on .bottomTrailing, but takes all the free space. Just try to remove or change position of Spacer's:

SwiftUI ScrollView does not center content when content fits scrollview bounds

I'm trying to have the content inside a ScrollView be centered when that content is small enough to not require scrolling, but instead it aligns to the top. Is this a bug or I'm missing adding something? Using Xcode 11.4 (11E146)
#State private var count : Int = 100
var body : some View {
// VStack {
ScrollView {
VStack {
Button(action: {
if self.count > 99 {
self.count = 5
} else {
self.count = 100
}
}) {
Text("CLICK")
}
ForEach(0...count, id: \.self) { no in
Text("entry: \(no)")
}
}
.padding(8)
.border(Color.red)
.frame(alignment: .center)
}
.border(Color.blue)
.padding(8)
// }
}
Credit goes to #Thaniel for finding the solution. My intention here is to more fully explain what is happening behind the scenes to demystify SwiftUI and explain why the solution works.
Solution
Wrap the ScrollView inside a GeometryReader so that you can set the minimum height (or width if the scroll view is horizontal) of the scrollable content to match the height of the ScrollView. This will make it so that the dimensions of the scrollable area are never smaller than the dimensions of the ScrollView. You can also declare a static dimension and use it to set the height of both the ScrollView and its content.
Dynamic Height
#State private var count : Int = 5
var body: some View {
// use GeometryReader to dynamically get the ScrollView height
GeometryReader { geometry in
ScrollView {
VStack(alignment: .leading) {
ForEach(0...self.count, id: \.self) { num in
Text("entry: \(num)")
}
}
.padding(10)
// border is drawn before the height is changed
.border(Color.red)
// match the content height with the ScrollView height and let the VStack center the content
.frame(minHeight: geometry.size.height)
}
.border(Color.blue)
}
}
Static Height
#State private var count : Int = 5
// set a static height
private let scrollViewHeight: CGFloat = 800
var body: some View {
ScrollView {
VStack(alignment: .leading) {
ForEach(0...self.count, id: \.self) { num in
Text("entry: \(num)")
}
}
.padding(10)
// border is drawn before the height is changed
.border(Color.red)
// match the content height with the ScrollView height and let the VStack center the content
.frame(minHeight: scrollViewHeight)
}
.border(Color.blue)
}
The bounds of the content appear to be smaller than the ScrollView as shown by the red border. This happens because the frame is set after the border is drawn. It also illustrates the fact that the default size of the content is smaller than the ScrollView.
Why Does it Work?
ScrollView
First, let's understand how SwiftUI's ScrollView works.
ScrollView wraps it's content in a child element called ScrollViewContentContainer.
ScrollViewContentContainer is always aligned to the top or leading edge of the ScrollView depending on whether it is scrollable along the vertical or horizontal axis or both.
ScrollViewContentContainer sizes itself according to the ScrollView content.
When the content is smaller than the ScrollView, ScrollViewContentContainer pushes it to the top or leading edge.
Center Align
Here's why the content gets centered.
The solution relies on forcing the ScrollViewContentContainer to have the same width and height as its parent ScrollView.
GeometryReader can be used to dynamically get the height of the ScrollView or a static dimension can be declared so that both the ScrollView and its content can use the same parameter to set their horizontal or vertical dimension.
Using the .frame(minWidth:,minHeight:) method on the ScrollView content ensures that it is never smaller than the ScrollView.
Using a VStack or HStack allows the content to be centered.
Because only the minimum height is set, the content can still grow larger than the ScrollView if needed, and ScrollViewContentContainer retains its default behavior of aligning to the top or leading edge.
You observe just normal ScrollView behaviour. Here is a demo of possible approach to achieve your goal.
// view pref to detect internal content height
struct ViewHeightKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
// extension for modifier to detect view height
extension ViewHeightKey: ViewModifier {
func body(content: Content) -> some View {
return content.background(GeometryReader { proxy in
Color.clear.preference(key: Self.self, value: proxy.size.height)
})
}
}
// Modified your view for demo
struct TestAdjustedScrollView: View {
#State private var count : Int = 100
#State private var myHeight: CGFloat? = nil
var body : some View {
GeometryReader { gp in
ScrollView {
VStack {
Button(action: {
if self.count > 99 {
self.count = 5
} else {
self.count = 100
}
}) {
Text("CLICK")
}
ForEach(0...self.count, id: \.self) { no in
Text("entry: \(no)")
}
}
.padding(8)
.border(Color.red)
.frame(alignment: .center)
.modifier(ViewHeightKey()) // read content view height !!
}
.onPreferenceChange(ViewHeightKey.self) {
// handle content view height
self.myHeight = $0 < gp.size.height ? $0 : gp.size.height
}
.frame(height: self.myHeight) // align own height with content
.border(Color.blue)
.padding(8)
}
}
}
The frame(alignment: .center) modifier you’ve added doesn’t work since what it does is wrapping your view in a new view of exactly the same size. Because of that the alignment doesn’t do anything as there is no additional room for the view do be repositioned.
One potential solution for your problem would be to wrap the whole ScrollView in a GeometryReader to read available height. Then use that height to specify that the children should not be smaller than it. This will then make your view centered inside of ScrollView.
struct ContentView: View {
#State private var count : Int = 100
var body : some View {
GeometryReader { geometry in
ScrollView {
VStack {
Button(action: {
if self.count > 99 {
self.count = 5
} else {
self.count = 100
}
}) {
Text("CLICK")
}
ForEach(0...self.count, id: \.self) { no in
Text("entry: \(no)")
}
}
.padding(8)
.border(Color.red)
.frame(minHeight: geometry.size.height) // Here we are setting minimum height for the content
}
.border(Color.blue)
}
}
}
For me, GeometryReader aligned things to the top no matter what. I solved it with adding two extra Spacers (my code is based on this answer):
GeometryReader { metrics in
ScrollView {
VStack(spacing: 0) {
Spacer()
// your content goes here
Spacer()
}
.frame(minHeight: metrics.size.height)
}
}

How do I change scrollview's scroll direction in SwiftUI?

I was trying to implement vertical scroll with image and text but I am not able to achieve it.
I tried on both Xcode beta 1 & 2.
Try to wrap both the Text and the Image in a VStack and be sure that there is enough content inside the ScrollView to fall outside it's bounds (in the right direction - vertically in your case):
ScrollView {
VStack {
ForEach (1...100) {_ in
Image(systemName: "circle.fill")
Text("my text")
}
}
}
You could easily try it out in a Playground like this :
import SwiftUI
import PlaygroundSupport
struct LiveView : View {
var body: some View {
ScrollView {
VStack {
ForEach (1...100) {_ in
Image(systemName: "circle.fill")
Text("Some text")
}
}
}
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: LiveView())