How does ScrollView work when wrapping a ZStack? - swiftui

In the code below, firstScrollProxy does not work, while secondScrollViewProxy does. I don't understand why.
The only solution I found, was to give some id to the overlay, and scroll to that. However that causes other issues for my code, and I'd rather avoid such workarounds.
I played with fixedSize() for the ZStack items, but that didn't help either.
Laying out the items vertically has the same issue, while a VStack works.
The anchor is optional, but trying different anchors does reveal the fact that the scroll view behaves as if the width of the items are the same as the entire scrollable area.
import SwiftUI
struct ContentView: View {
var body: some View {
let numItems: Int = 100
let itemWidth = 60.0
let itemHeight = 100.0
VStack(spacing: 4) {
Spacer()
ScrollViewReader { firstScrollProxy in
ScrollView(.horizontal) {
ZStack {
ForEach(0..<numItems, id:\.self) { x in
Rectangle()
.fill(Color.purple)
.frame(width: itemWidth - 2, height: itemHeight)
.overlay {
Text("\(x)")
}
.position(x: Double(x) * itemWidth + itemWidth / 2.0, y: itemHeight / 2.0)
}
}
.frame(width: Double(numItems) * itemWidth, height: itemHeight)
}
.onTapGesture {
withAnimation {
firstScrollProxy.scrollTo(17, anchor: .center)
}
}
}
.padding(8)
.background(Color(white: 0.2))
Color.clear.frame(height: 10)
ScrollViewReader { secondScrollProxy in
ScrollView(.horizontal) {
HStack(spacing: 2) {
ForEach(0..<numItems, id:\.self) { x in
Rectangle()
.fill(Color.blue)
.frame(width: itemWidth - 2, height: itemHeight)
.overlay {
Text("\(x)")
}
}
}
.frame(height: itemHeight)
}
.onTapGesture {
withAnimation {
secondScrollProxy.scrollTo(17, anchor: .center)
}
}
}
.padding(8)
.background(Color(white: 0.25))
Spacer()
}
.background(.black)
.foregroundColor(.white)
}
}

ZStack doesn't know about the effect of the position modifier. ZStack just assumes all of its children are piled up on top of each other, since that's how it lays them out. So when the ZStack's parent ScrollView asks the ZStack for its size, the ZStack reports a size that is the maximum width and height of any of its children, without accounting for the side-by-side layout you have manually implemented.

Given Rob's answer, the solution is actually to use the anchor to calculate the the position to scroll to. E.g.
ZStack {
// ...
}
.id("zstack")
.onTapGesture {
.withAnimation {
let x = position_of_17 / width_of_zstack
firstScrollProxy.scrollTo("zstack", anchor: UnitPoint(x: x, y: 1.0))
}
}
}

I found a better workaround, if anyone should have this problem. Simply embed a HStack with a width equal to the ZStack in which you can add invisible elements with the ids and the location that you need to scroll to.
In my case this is acceptable, I only ever need to programmatically scroll to 1 element, and switching to an actual HStack view for my content would not make sense for my use case.
ZStack {
// Content
HStack(spacing: 0) {
Color.clear
.frame(width: targetX, height: 1)
Color.clear
.frame(width: itemWidth, height: 1)
.id("target")
Color.clear
.frame(width: totalWidth - targetX - itemWidth, height: 1)
}
}
func scrollToTarget() {
scrollProxy.scrollTo("target", anchor: anchor)
}

Related

SwiftUI How to make a slide in view that pushes another view away while keeping it the same size

Before Slide
After Slide
struct SlideView: View {
#State var isClicked = false
var body: some View {
HStack{
Rectangle()
.fill(.gray)
.ignoresSafeArea()
.frame(width: isClicked ? UIScreen.main.bounds.width * 0.75 : 0)
VStack{
Text("This gets squished")
HStack{
Button{
withAnimation(.spring()) {
isClicked.toggle()
}
} label: {
Image(systemName: "menucard.fill")
.padding(.leading)
}
Spacer()
}
Spacer()
}
}
}
}
So I have this view that kind of "slides in" and it's pretty close to what I want but not exactly, the view that slides in ends up squishing the other view and compressing it to be about 25% of it's size. Of course this makes sense because it's all in a HStack and I end up taking 75% of the HStack's space but I was wondering if anybody had an idea of how I could do something like this but without squishing the second view that gets pushed away? So basically keep it the same size and just have the first 1/4 of the view visible and the other 3/4 just be gone I guess.
The problem is the view that gets squished has no explicit width. Understand how views get sized in SwiftUI. The parent view proposes a size, and the child view responds with the size it wants. Before you click the button, the parent view (the HStack) is proposing 100% to the view. That view says that I can do 100%, so no squishing. However, when you make the other view 75% of the parent, the parent can only offer 25% to the first view. The view responds, I can fit if I squish, so it does. See Laying out a simple view.
To fix it, you simply need to explicitly set the width of the first view to 100%, and then it will get pushed over like you were expecting without compression.
struct SlideInView: View {
#State var isClicked = false
var body: some View {
// I swapped out UIScreen.main.bounds for a GeometryReader
// UIScreen.main.bounds.width always returns the width of the screen
// even if the parent view does not have that much space to offer, and
// can lead to some interesting results.
GeometryReader { geometry in
HStack{
Rectangle()
.fill(.gray)
.ignoresSafeArea()
.frame(width: isClicked ? geometry.size.width * 0.75 : 0)
VStack{
Text("This gets squished")
HStack{
Button{
withAnimation(.spring()) {
isClicked.toggle()
}
} label: {
Image(systemName: "menucard.fill")
.padding(.leading)
}
Spacer()
}
Spacer()
}
.frame(width: geometry.size.width) // Explicitly set width here
}
}
}
}
Personally, I think it would make more sense to just use .offset, and put them in a ZStack:
ZStack {
VStack {
HStack {
Spacer()
Text("yellow")
Spacer()
}
Spacer()
}
.background(.yellow)
VStack {
Text("red")
HStack {
Button{
withAnimation(.spring()) {
isClicked.toggle()
}
} label: {
Image(systemName: "menucard.fill")
.padding(.leading)
}
Spacer()
}
Spacer()
}
.background(.red)
.offset(.init(width: isClicked ? UIScreen.main.bounds.width * 0.75 : 0, height: 0))
}
And you can add another .offset modifier to the yellow one to make it looks slidein:
VStack {
//...
}
.background(.yellow)
.offset(.init(width: isClicked ? 0 : -UIScreen.main.bounds.width * 0.75, height: 0))

Transparent background SwiftUI

I have a list in swiftUI and I want the background to be somewhat see through. However there seems to be a white background applied to both scrollview and List. I was wondering if anyone had a work around or way to change it so when .background(Color.white.opacity(0.7)) is applied to the list, it can be translucent and not have the same affect as .background(Color.white).
Here is my code for the view that I am trying to implement the desired effect. The wordPosts is simply just a custom data struct used to populate the list item data. I've already trying changing the cell and tableview appearance using UIkit on init which did not work.
struct ProfileWordsView: View {
init(){
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
}
var body: some View {
List {
ForEach(wordPosts) { post in
Group {
VStack(alignment: .leading) {
HStack(alignment: .top) {
RoundedRectangle(cornerRadius: 5)
.fill()
.frame(width: 4)
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 5) {
Text(post.title).font(.custom("Gilroy-SemiBold", size: 20)).foregroundColor(.black).lineLimit(1)
Text("• \(post.timestamp)").font(.custom("Gilroy-SemiBold", size: 19)).foregroundColor(.gray)
}
HStack {
Text(post.text).font(.custom("Gilroy-Regular", size: 16)).foregroundColor(Color.black.opacity(0.7)).padding(.top, 5)
}
}
Spacer()
VStack {
Button(action: {}) {
Image(systemName: "chevron.up")
.resizable()
.frame(width: 20, height: 15)
.font(Font.title3)
.foregroundColor(.gray)
.padding(.bottom, 6)
}
Text("\(post.rizz)").font(.custom("Gilroy-SemiBold", size: 18)).foregroundColor(.gray)
Button(action: {}) {
Image(systemName: "chevron.down")
.resizable()
.frame(width: 20, height: 15)
.font(Font.title3)
.foregroundColor(.gray)
}
}
}
}.listRowInsets(EdgeInsets(top: 10, leading: 5, bottom: 10, trailing: 10))
}.listRowBackground(Color.clear)
}
.listRowSeparator(.hidden)
}.edgesIgnoringSafeArea(.all).listStyle(.plain)
}
}
I'm not sure if it is possible for your code but I'd recommend using a scrollview instead. They are transparent by default, so that should help. You can always add a frame with a semi-transparent color if you want to have a semi-transparent background instead.

SwiftUI, remove space between views in a VStack?

Why is there so much space between the three blue rectangles and the list? How can I remove the space so that all views within the VStack stack at the top? I tried using a Spacer() directly after the List, but nothing changed.
struct ContentView: View {
init() { UITableView.appearance().backgroundColor = UIColor.clear }
var body: some View {
NavigationView {
ZStack {
Color.red
.ignoresSafeArea()
VStack {
HStack {
Text("Faux Title")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.heavy)
Spacer()
Button(action: {
// settings
}, label: {
Image(systemName: "gearshape.fill")
.font(.system(.title2))
})
}
.padding()
GeometryReader { geometry in
HStack() {
Text("1")
.frame(width: geometry.size.width * 0.30, height: 150)
.background(Color.blue)
Spacer()
Text("2")
.frame(width: geometry.size.width * 0.30, height: 150)
.background(Color.blue)
Spacer()
Text("3")
.frame(width: geometry.size.width * 0.30, height: 150)
.background(Color.blue)
}
}
.padding()
List {
Text("One")
Text("Two")
Text("Three")
Text("Four")
Text("Five")
Text("Six")
}
.listStyle(InsetGroupedListStyle())
}
}
.navigationBarHidden(true)
}
}
}
Bonus question: In web development, you can open your browser's Web Inspector and use the element selector to click on elements which highlights their borders. Useful for something like this where you're trying to figure out which element the offending spacing belongs to. Is there something like that in Xcode?
VStack(spacing: 0) {...}
Spacer()
to your question you can in Xcode use the view inspector. https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/ExaminingtheViewHierarchy.html
Since you know that your HStack with the blue rectangles is going to be a height of 150, you should constrain it to that using .frame(height: 150):
GeometryReader { geometry in
...
}
.padding()
.frame(height: 150) //Here
Otherwise, the GeometryReader will occupy all available vertical space.
Re: your web dev comparison, check out the Xcode view hierarchy inspector. It's not exactly the same, but it's in the same vein: https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/ExaminingtheViewHierarchy.html

How to ensure view appears above other views when iterating with ForEach in SwiftUI?

I have a SwiftUI view that is a circular view which when tapped opens up and is supposed to extend over the UI to its right. How can I make sure that it will appear atop the other ui? The other UI elements were created using a ForEach loop. I tried zindex but it doesn't do the trick. What am I missing?
ZStack {
VStack(alignment: .leading) {
Text("ALL WORKSTATIONS")
ZStack {
ChartBackground()
HStack(alignment: .bottom, spacing: 15.0) {
ForEach(Array(zip(1..., dataPoints)), id: \.1.id) { number, point in
VStack(alignment: .center, spacing: 5) {
DataCircle().zIndex(10)
ChartBar(percentage: point.percentage).zIndex(-1)
Text(point.month)
.font(.caption)
}
.frame(width: 25.0, height: 200.0, alignment: .bottom)
.animation(.default)
}
}
.offset(x: 30, y: 20)
}
.frame(width: 500, height: 300, alignment: .center)
}
}
}
}
.zIndex have effect for views within one container. So to solve your case, as I assume expanded DataCircle on click, you need to increase zIndex of entire bar VStack per that click by introducing some kind of handling selection.
Here is simplified replicated demo to show the effect
struct TestBarZIndex: View {
#State private var selection: Int? = nil
var body: some View {
ZStack {
VStack(alignment: .leading) {
Text("ALL WORKSTATIONS")
ZStack {
Rectangle().fill(Color.yellow)//ChartBackground()
HStack(alignment: .bottom, spacing: 15.0) {
ForEach(1...10) { number in
VStack(spacing: 5) {
Spacer()
ZStack() { // DataCircle()
Circle().fill(Color.pink).frame(width: 20, height: 20)
.onTapGesture { self.selection = number }
if number == self.selection {
Text("Top Description").fixedSize()
}
}
Rectangle().fill(Color.green) // ChartBar()
.frame(width: 20, height: CGFloat(Int.random(in: 40...150)))
Text("Jun")
.font(.caption)
}.zIndex(number == self.selection ? 1 : 0) // << here !!
.frame(width: 25.0, height: 200.0, alignment: .bottom)
.animation(.default)
}
}
}
.frame(height: 300)
}
}
}
}

SwiftUI GeometryReader compact size

I would like my LoadingTitle to have a width of 70% of the screen so I use the GeometryReader but it makes the vertical size expand and my LoadingTitle takes much more vertical space. I would like it to remain as compact as possible.
When using hardcoded width: 300 I get the correct layout (except the relative width):
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
LoadingTitle()
Color.blue
}
}
}
struct LoadingTitle: View {
var body: some View {
HStack() {
Color.gray
}
.frame(width: 300, height: 22)
.padding(.vertical, 20)
.border(Color.gray, width: 1)
}
}
Now if I wrap the body of my LoadingTitle in the GeometryReader I can get the correct relative size but then the GeometryReader expands my view vertically:
struct LoadingTitle: View {
var body: some View {
GeometryReader { geo in
HStack() {
Color.gray
.frame(width: geo.size.width * 0.7, height: 22, alignment: .leading)
Spacer()
}
.padding(.vertical, 20)
.border(Color.gray, width: 1)
}
}
}
I tried using .fixedSize(horizontal: false, vertical: true) on the GeometryReader as other suggested but then the resulting view is too much compact and all its paddings are ignored:
How could I achieve the layout of the 1st screenshot with a relative width?
Here is possible approach. Tested with Xcode 11.4 / iOS 13.4 (w/ ContentView unchanged)
struct LoadingTitle: View {
var body: some View {
VStack { Color.clear }
.frame(height: 22).frame(maxWidth: .infinity)
.padding(.vertical, 20)
.overlay(
GeometryReader { geo in
HStack {
HStack {
Color.gray
.frame(width: geo.size.width * 0.7, height: 22)
}
.padding(.vertical, 20)
.border(Color.gray, width: 1)
Spacer()
}
}
)
}
}
Since you have a fixed height for title,
struct LoadingTitle1: View {
var body: some View {
GeometryReader { geo in
HStack {
Color.gray.frame(width: geo.size.width * 0.7)
.padding(.vertical, 20)
.border(Color.gray, width: 1)
Spacer()
}
}.frame(height: 62)
}
}