Reading UI screen height inside scrollview using GeometryReader - swiftui

I have a scrollview with a few subviews, and I wanted each of these subviews to have a specific height relative to UI screen height. I know you can do this with GeometryReader, but the moment I put GeometryReader inside ScrollView, it fails to read screen height.
What I want to achieve
Code and preview without ScrollView
struct CurrentView: View {
var body: some View {
NavigationView {
GeometryReader { geo in
VStack {
Rectangle()
.foregroundColor(Color.red.opacity(0.5))
.frame(maxWidth: geo.size.width, maxHeight: geo.size.height * 0.3)
Rectangle()
.foregroundColor(Color.red.opacity(0.5))
.frame(maxWidth: geo.size.width, maxHeight: geo.size.height * 0.3)
Rectangle()
.foregroundColor(Color.red.opacity(0.5))
.frame(maxWidth: geo.size.width, maxHeight: geo.size.height * 0.3)
}
}
.navigationTitle("Title")
.padding(.horizontal)
}
}
}
Code and Preview with scrollview
struct CurrentView: View {
var body: some View {
NavigationView {
GeometryReader { geo in
ScrollView {
VStack {
Rectangle()
.foregroundColor(Color.red.opacity(0.5))
.frame(maxWidth: geo.size.width, maxHeight: geo.size.height * 0.3)
Rectangle()
.foregroundColor(Color.red.opacity(0.5))
.frame(maxWidth: geo.size.width, maxHeight: geo.size.height * 0.3)
Rectangle()
.foregroundColor(Color.red.opacity(0.5))
.frame(maxWidth: geo.size.width, maxHeight: geo.size.height * 0.3)
}
}
}
.navigationTitle("Title")
.padding(.horizontal)
}
}
}
The view seems to break whenever I use ScrollView and GeometryReader's height together. I tried putting ScrollView inside GeometryReader, and the other way around as well, but with the same results (geo.size.height becomes 0 it seems).
So is there a way to set frame height relative to screen height for frames inside a scrollview? Or will I have to just resort to UIScreen.main.bounds.size.height Thank you!

This happens because you use maxHeight instead of height. The ScrollView will always try to "collapse" its content and since you are using the GeometryReader to dynamically calculate the height, you need to set it explicitly to tell the ScrollView that this is the size you always want. minHeight would also do the trick.
struct ContentView: View {
var body: some View {
GeometryReader { proxy in
ScrollView {
VStack {
Rectangle()
.foregroundColor(Color.red.opacity(0.5))
.frame(height: proxy.size.height * 0.3)
Rectangle()
.foregroundColor(Color.red.opacity(0.5))
.frame(height: proxy.size.height * 0.3)
Rectangle()
.foregroundColor(Color.red.opacity(0.5))
.frame(height: proxy.size.height * 0.3)
}
}
}
}
}

Related

How does ScrollView work when wrapping a ZStack?

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

SwiftUI DragGesture is freeze

I tried to do a resizable view with a gesture.
The problem is that when I move my gesture object left or right the app will be freeze with 100% processor usage. It is the loop between two Text views.
What did I do wrong, and how do I make this correct?
struct TestView2 : View {
#State private var width : CGFloat = 400.0
var body : some View {
VStack {
ZStack(alignment: .bottomTrailing) {
Text("\(width)")
.frame(width: width)
Text(":")
.frame(width: 10, height: 30)
.background(.bar)
.gesture(
DragGesture()
.onChanged { value in
width = max(100, width + value.translation.width)
print(width)
}
)
}
.frame(width: width, height: 30, alignment: .topLeading)
.border(.gray, width: 1)
.background(.green)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
View:
Debugger:
For me, the solution was to explicitly set the gesture's coordinateSpace to .global:
DragGesture(coordinateSpace: .global)
.onChanged { ... }

How to change the height of the object by using DragGesture in SwiftUI?

I'm trying to increase the height of the shape by using the "dragger" (rounded grey rectangle) element. I use the DragGesture helper provided by SwiftUI to get the current position of the user finger. Unfortunately, during the drag event content is jumping for some reason.
Could you please help me to find the root cause of the problem?
This is how it looks during the drag event
If I remove Spacer() everything is okay but not what I wanted
This is my code snippets
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
CustomDraggableComponent()
Spacer() // If comment this line the result will be as on the bottom GIF example
}
}
import SwiftUI
let MIN_HEIGHT = 50
struct CustomDraggableComponent: View {
#State var height: CGFloat = MIN_HEIGHT
var body: some View {
VStack {
Rectangle()
.fill(Color.red)
.frame(width: .infinity, height: height)
HStack {
Spacer()
Rectangle()
.fill(Color.gray)
.frame(width: 100, height: 10)
.cornerRadius(10)
.gesture(
DragGesture()
.onChanged { value in
height = value.translation.height + MIN_HEIGHT
}
)
Spacer()
}
}
}
The correct calculation is
.gesture(
DragGesture()
.onChanged { value in
height = max(MIN_HEIGHT, height + value.translation.height)
}
)
Also, remove the infinite width. It's invalid.
.frame(minHeight: 0, maxHeight: height)
or
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: height)

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

how to strech image on one side in swiftUI, like a .9 file in Android?

i have an image that i want to strech only it's height to fit different content, how do i do that in swiftUI? right now it looks like this
struct ContentView: View {
var body: some View {
VStack {
HStack {
VStack(alignment: .leading, spacing: 130) {
Text("Title")
.font(.headline)
.foregroundColor(.primary)
Text("text")
.font(.subheadline)
.foregroundColor(.secondary)
Text("padding")
}
.padding(.vertical)
Spacer()
Image("rightTag")
.resizable(capInsets: .init(top: 0, leading: 0, bottom: 0, trailing: 0), resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 20)
}
.frame(maxWidth: screen.width - 60)
.padding(.leading)
.background(.white)
.cornerRadius(20)
}
}
}
how can i stretch its height to fit this outer frame? ragular resizable and stuff can't get it done.
any helped would be wonderful! Thanks!
sry i didn't make myself clear earllier.
Here is possible approach. However as I see now you'd rather need to stretch not the entire original image, but only middle of it, so in real project it would be needed to make your image tri-part and apply below stretching approach only to middle (square) part.
Approach uses asynchronous state update, so works in Live Preview / Simulator / RealDevice. (Tested with Xcode 11.2 / iOS 13.2)
struct ContentView: View {
#State private var textHeigh: CGFloat = .zero
var body: some View {
VStack {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 130) {
Text("Title")
.font(.headline)
.foregroundColor(.primary)
Text("text")
.font(.subheadline)
.foregroundColor(.secondary)
Text("padding")
}
.padding(.vertical)
.alignmentGuide(.top, computeValue: { d in
DispatchQueue.main.async {
self.textHeigh = d.height // !! detectable height of left side text
}
return d[.top]
})
Spacer()
Image("rightTag")
.resizable()
.frame(maxWidth: 20, maxHeight: max(60, textHeigh)) // 60 just for default
}
.frame(maxWidth: screen.width - 60)
.padding(.leading)
.background(Color.white)
}
}
}