SwiftUI offset items in ScrollView under another view - swiftui

I have a VStack with some content at the top of my app, then a ScrollView on the bottom, with these views being seperated with a Divider. Is there any way to offset the scrollView such that it starts slightly tucked under the Divider and the top view?
Here is an example image of what I want:
The numbers are in a ScrollView and the top content is simply Color.white in this example.
If I apply a simply y offset, though, I get this:
The number is vertically shifted up, but not "tucked" under.
Is there an easy way to get the "tucked" result? I'm sure I could use a ZStack or something, but that seems like a lot of work, especially because I don't know how large the top content will be.
Example Code:
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
Color.white.frame(height: 100)
Divider()
ScrollView {
ForEach(0..<20) { number in
Text("\(number)")
}
}
.offset(y: -8)
}
}
}

I assume you just need padding for scroll view, like
ScrollView {
ForEach(0..<20) { number in
Text("\(number)")
}
}
.padding(.top, -8) // << here !!
.clipped()

Related

How to solve this top bar problem on SwiftUI, elegantly?

I am trying to use the whole iPhone area for my app.
I have this HStack at the top, used to create a custom toolbar.
var body: some View {
VStack (spacing:0) {
MyTopbar()
// other controls
Spacer()
}
.edgesIgnoringSafeArea(.top)
This appears like this on new devices with a notch and old devices without a notch. The notch cuts my menu.
I can solve that by adding a spacer with a frame height before MyTopbar() on the vertical stack but first of all this seems to be a very awful solution. First I have to guess a height for that spacer. Then I have to detect if the device has a notch or not (?).
Is there a better way?
You can think of it as layers (content that respects safe area and content that doesn't).
Something like this perhaps:
struct ContentView: View {
var body: some View {
ZStack {
Color.blue.ignoresSafeArea() // Whatever view fills the whole screen
VStack (spacing:0) {
MyTopbar()
// other controls
Spacer()
}
}
}
}
A possible solution to add clear color with safe area height. No need for much calculation.
var body: some View {
VStack (spacing:0) {
Color.clear.frame(height: Color.clear.frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
MyTopbar()
// other controls
Spacer()
}
.edgesIgnoringSafeArea(.top)

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.

Underline content of a VStack

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

Animated transitions and contents within ScrollView in SwiftUI

I'm not quite a SwiftUI veteran but I've shipped a couple of apps of moderate complexity. Still, I can't claim that I fully understand it and I'm hoping someone with deeper knowledge could shed some light on this issue:
I have some content that I want to toggle on and off, not unlike .sheet(), but I want more control over it. Here is some "reconstructed" code but it should be able capture the essence:
struct ContentView: View {
#State private var isShown = false
var body: some View {
GeometryReader { g in
VStack {
ZStack(alignment: .top) {
// This element "holds" the size
// while the content is hidden
Color.clear
// Content to be toggled
if self.isShown {
ScrollView {
Rectangle()
.aspectRatio(1, contentMode: .fit)
.frame(width: g.size.width) // This is a "work-around"
} // ScrollView
.transition(.move(edge: .bottom))
.animation(.easeOut)
}
} // ZStack
// Button to show / hide the content
Button(action: {
self.isShown.toggle()
}) {
Text(self.isShown ? "Hide" : "Show")
}
} // VStack
} // GeometryReader
}
}
What it does is, it toggles on and off some content block (represented here by a Rectangle within a ScrollView). When that happens, the content view in transitioned by moving in from the bottom with some animation. The opposite happens when the button is tapped again.
This particular piece of code works as intended but only because of this line:
.frame(width: g.size.width) // This is a "work-around"
Which, in turn, requires an extra GeometryReader, otherwise, the width of the content is animated, producing an unwanted effect (another "fix" I've discovered is using the .fixedSize() modifier but, to produce reasonable effects, it requires content that assumes its own width like Text)
My question to the wise is: is it possible to nicely transition in content encapsulated within a ScrollView without using such "fixes"? Alternatively, is there a more elegant fix for that?
A quick addition to the question following #Asperi's answer: contents should remain animatable.
You are my only hope,
–Baglan
Here is a solution (updated body w/o GeometryReader). Tested with Xcode 11.4 / iOS 13.4
var body: some View {
VStack {
ZStack(alignment: .top) {
// This element "holds" the size
// while the content is hidden
Color.clear
// Content to be toggled
if self.isShown {
ScrollView {
Rectangle()
.aspectRatio(1, contentMode: .fit)
.animation(nil) // << here !!
} // ScrollView
.transition(.move(edge: .bottom))
.animation(.easeOut)
}
} // ZStack
// Button to show / hide the content
Button(action: {
self.isShown.toggle()
}) {
Text(self.isShown ? "Hide" : "Show")
}
} // VStack
}

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: