Using SwiftUI, I am trying to center a View on the screen and then give it a header and/or footer of variable heights.
Using constraints it would look something like this:
let view = ...
let header = ...
let footer = ...
view.centerInParent()
header.pinBottomToTop(of: view)
footer.pinTopToBottom(of: view)
This way, the view would always be centered on the screen, regardless of the size of the header and footer.
I cannot figure out how to accomplish this with SwiftUI. Using any type of HStack or VStack means the sizes of the header and footer push around the view. I would like to avoid hardcoding any heights since the center view may vary in size as well.
Any ideas? New to SwiftUI so advice is appreciated!
If I correctly understood your goal (because, as #nayem commented, at first time seems I missed), the following approach should be helpful.
Code snapshot:
extension VerticalAlignment {
private enum CenteredMiddleView: AlignmentID {
static func defaultValue(in dimensions: ViewDimensions) -> CGFloat {
return dimensions[VerticalAlignment.center]
}
}
static let centeredMiddleView = VerticalAlignment(CenteredMiddleView.self)
}
extension Alignment {
static let centeredView = Alignment(horizontal: HorizontalAlignment.center,
vertical: VerticalAlignment.centeredMiddleView)
}
struct TestHeaderFooter: View {
var body: some View {
ZStack(alignment: .centeredView) {
Rectangle().fill(Color.clear) // !! Extends ZStack to full screen
VStack {
Header()
Text("I'm on center")
.alignmentGuide(.centeredMiddleView) {
$0[VerticalAlignment.center]
}
Footer()
}
}
// .edgesIgnoringSafeArea(.top) // uncomment if needed
}
}
struct Header: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 40)
}
}
struct Footer: View {
var body: some View {
Rectangle()
.fill(Color.green)
.frame(height: 200)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
TestHeaderFooter()
}
}
Here's the code:
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
Rectangle()
.fill(Color.gray)
.frame(width: geometry.size.width, height: geometry.size.height * 0.1, alignment: .center)
Text("Center")
.frame(width: geometry.size.width, height: geometry.size.height * 0.2, alignment: .center)
Rectangle()
.fill(Color.gray)
.frame(width: geometry.size.width, height: geometry.size.height * 0.1, alignment: .center)
}
}
}
}
using GeometryReader you can apply the dynamic size for your views.
also here is screenshot for above code
put Spacer() between header view and footer view.
headerview()
Spacer()
footerview()
Related
I've got a simple HStack with subviews inside. How can I tell the first subview to be 60% the size of the HStack without using a GeometryReader?
struct ContentView: View {
var body: some View {
HStack {
Color.red.opacity(0.3)
Color.brown.opacity(0.4)
Color.yellow.opacity(0.6)
}
}
}
The code above makes each subview the same size. But I want the first one to be 60% regardless of it's content. In this example, it is a color, but it could be anything.
The HStack is dynamic in size.
Edit: Why no GeometryReader?
When I want to place multiple of those HStacks inside a ScrollView, they overlap, because the GeometryReader's height is only 10 Point. As mentioned above, the Color views could be anything, so I used VStacks with cells in it that have dynamic heights.
struct ContentView: View {
var body: some View {
ScrollView(.vertical) {
ProblematicView()
ProblematicView()
}
}
}
struct ProblematicView: View {
var body: some View {
GeometryReader { geo in
HStack(alignment: .top) {
VStack {
Rectangle().frame(height: 20)
Rectangle().frame(height: 30)
Rectangle().frame(height: 20)
Rectangle().frame(height: 40)
Rectangle().frame(height: 20)
}
.foregroundColor(.red.opacity(0.3))
.frame(width: geo.size.width * 0.6)
.overlay(Text("60%").font(.largeTitle))
VStack {
Rectangle().frame(height: 10)
Rectangle().frame(height: 30)
Rectangle().frame(height: 20)
}
.foregroundColor(.brown.opacity(0.4))
.overlay(Text("20%").font(.largeTitle))
VStack {
Rectangle().frame(height: 5)
Rectangle().frame(height: 10)
Rectangle().frame(height: 24)
Rectangle().frame(height: 10)
Rectangle().frame(height: 17)
Rectangle().frame(height: 13)
Rectangle().frame(height: 10)
}
.foregroundColor(.yellow.opacity(0.6))
.overlay(Text("20%").font(.largeTitle))
}
}
.border(.blue, width: 3.0)
}
}
As you can see, the GeometryReader's frame is too small in height. It should be as high as the HStack. That causes the views to overlap.
I don't know the exact reason (might be a bug in GeometryReader), but placing the GeometryReader outside the ScrollView, and passing down its width makes your code behave as you expect.
struct ContentView: View {
var body: some View {
GeometryReader { geo in
ScrollView {
ProblematicView(geoWidth: geo.size.width)
ProblematicView(geoWidth: geo.size.width)
}
}
.border(.blue, width: 3.0)
}
}
struct ProblematicView: View {
let geoWidth: CGFloat
var body: some View {
// same code, but using geoWidth to compute the relative width
Result:
You can set by .frame & UIScreen.main.bounds.size.width * (your width ratio) calculation.
Example
struct ContentView: View {
var body: some View {
HStack {
Color.red.opacity(0.3)
.frame(width: UIScreen.main.bounds.size.width * 0.6, height: nil)
Color.purple.opacity(0.4)
.frame(width: UIScreen.main.bounds.size.width * 0.2, height: nil)
Color.yellow.opacity(0.6)
.frame(width: UIScreen.main.bounds.size.width * 0.2, height: nil)
}
}
}
Using GeometryReader
struct ContentView: View {
var body: some View {
GeometryReader { geo in
HStack {
Color.red.opacity(0.3)
.frame(width: geo.size.width * 0.6, height: nil)
Color.brown.opacity(0.4)
.frame(width: geo.size.width * 0.2, height: nil)
Color.yellow.opacity(0.6)
.frame(width: geo.size.width * 0.2, height: nil)
}
}
}
}
I have an issue similar to SwiftUI: minimumScaleFactor not applying evenly to stack elements, but in my case I can't get even scaling of elements with my HStack.
I want:
struct ScaleWithSymbol: View {
var body: some View {
HStack(spacing: 0) {
Group {
Text("XX°")
Text("↑")
.scaleEffect(0.75)
}
}
.lineLimit(1)
}
}
struct ScaleWithSymbol_Previews: PreviewProvider {
static var previews: some View {
ScaleWithSymbol()
.frame(height: 8)
// .frame(width: 18)
.scaledToFit()
.minimumScaleFactor(0.1)
}
}
If I force height, things scale evenly (.frame(height: 8)) and it yields the screenshot above.
But if I force width instead via .frame(width: 18) -- my use case where I then want to force the view to a given width -- I get:
What is the best approach to scale to fixed width but evenly? Thanks.
Weirdly enough, the problem is this line:
.lineLimit(1)
After removing that, the code works. Here is an interactive example with a Slider:
struct ContentView: View {
#State private var width: CGFloat = 50
var body: some View {
VStack {
ScaleWithSymbol()
.scaledToFit()
.minimumScaleFactor(0.1)
.frame(width: width)
Slider(value: $width, in: 1 ... 60)
}
}
}
struct ScaleWithSymbol: View {
var body: some View {
HStack(spacing: 0) {
Text("XX°")
Text("↑").scaleEffect(0.75)
}
}
}
I have a similar problem to this question (no answer yet): SwiftUI HStack with GeometryReader and paddings
In difference my goal is to align two views inside an HStack and where the left view gets 1/3 of the available width and the right view gets 2/3 of the available width.
Using GeometryReader inside the ChildView messes up the whole layout, because it fills up the height.
This is my example code:
struct ContentView: View {
var body: some View {
VStack {
VStack(spacing: 5) {
ChildView().background(Color.yellow.opacity(0.4))
ChildView().background(Color.yellow.opacity(0.4))
Spacer()
}
.padding()
Spacer()
Text("Some random Text")
}
}
}
struct ChildView: View {
var body: some View {
GeometryReader { geo in
HStack {
Text("Left")
.frame(width: geo.size.width * (1/3))
Text("Right")
.frame(width: geo.size.width * (2/3))
.background(Color.red.opacity(0.4))
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.green.opacity(0.4))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Which results in this:
Now If you would embed this view inside others views the layout is completely messed up:
e.g. inside a ScrollView
So how would one achieve the desired outcome of having a HStack-ChildView which fills up the space it gets and divides it (1/3, 2/3) between its two children?
EDIT
As described in the answer, I also forgot to add HStack(spacing: 0). Leaving this out is the reason for the right child container to overflow.
You can create a custom PreferenceKey for the view size. Here is an example:
struct ViewSizeKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
Then, create a view which will calculate its size and assign it to the ViewSizeKey:
struct ViewGeometry: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: ViewSizeKey.self, value: geometry.size)
}
}
}
Now, you can use them in your ChildView (even if it's wrapped in a ScrollView):
struct ChildView: View {
#State var viewSize: CGSize = .zero
var body: some View {
HStack(spacing: 0) { // no spacing between HStack items
Text("Left")
.frame(width: viewSize.width * (1 / 3))
Text("Right")
.frame(width: viewSize.width * (2 / 3))
.background(Color.red.opacity(0.4))
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.green.opacity(0.4))
.background(ViewGeometry()) // calculate the view size
.onPreferenceChange(ViewSizeKey.self) {
viewSize = $0 // assign the size to `viewSize`
}
}
}
I have some issues with the scrollview and GeometryReader. I want to have a list of items under an image. And each item should have the following width and height: ((width of the entire screen - leading padding - trailing padding) / 2).
I have tried two approaches for my use case. This is the code structure of my first one:
Approach #1
ScrollView
- VStack
- Image
- GeometryReader
- ForEach
- Text
I am using the geometry reader to get the width of the VStack as it has a padding and I don't want to have the full width of the scrollview.
But with the GeometryReader, only the last item from the ForEach loop is shown on the UI. And the GeometryReader has only a small height. See screenshot.
Code:
import SwiftUI
struct Item: Identifiable {
var id = UUID().uuidString
var value: String
}
struct ContentView: View {
func items() -> [Item] {
var items = [Item]()
for i in 0..<100 {
items.append(Item(value: "Item #\(i)"))
}
return items
}
var body: some View {
ScrollView {
VStack {
Image(systemName: "circle.fill")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.padding()
.foregroundColor(.green)
GeometryReader { geometry in
ForEach(self.items()) { item in
Text(item.value)
.frame(width: geometry.size.width / CGFloat(2), height: geometry.size.width / CGFloat(2))
.background(Color.red)
}
}
.background(Color.blue)
}
.frame(maxWidth: .infinity)
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The red color are the items in the ForEach loop. Blue the GeometryReader and green just the image.
Approach #2
ScrollView
-GeometryReader
- VStack
- Image
- ForEach
-Text
Then the items in my ForEach loop are rendered correctly but it's not possible to scroll anymore.
Code
import SwiftUI
struct Item: Identifiable {
var id = UUID().uuidString
var value: String
}
struct ContentView: View {
func items() -> [Item] {
var items = [Item]()
for i in 0..<100 {
items.append(Item(value: "Item #\(i)"))
}
return items
}
var body: some View {
ScrollView {
GeometryReader { geometry in
VStack {
Image(systemName: "circle.fill")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.padding()
.foregroundColor(.green)
ForEach(self.items()) { item in
Text(item.value)
.frame(width: geometry.size.width / CGFloat(2), height: geometry.size.width / CGFloat(2))
.background(Color.red)
}
}
.frame(maxWidth: .infinity)
}
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
How can I archive to have the UI correctly shown. Am I'm missing something here?
I would really appreciate some help.
Thank you.
EDIT
I found a workaround to have the UI correctly rendered with a working scrollView but that looks quite hacky to me.
I am using a PreferenceKey for this workaround.
I am using the geometryReader inside the scrollview with a height of 0. Only to get the width of my VStack.
On preferenceKeyChange I am updating a state variable and using this for my item to set the width and height of it.
import SwiftUI
struct Item: Identifiable {
var id = UUID().uuidString
var value: String
}
struct ContentView: View {
#State var width: CGFloat = 0
func items() -> [Item] {
var items = [Item]()
for i in 0..<100 {
items.append(Item(value: "Item #\(i)"))
}
return items
}
struct WidthPreferenceKey: PreferenceKey {
typealias Value = [CGFloat]
static var defaultValue: [CGFloat] = [0]
static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
value.append(contentsOf: nextValue())
}
}
var body: some View {
ScrollView {
VStack(spacing: 0) {
Image(systemName: "circle.fill")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.padding()
.foregroundColor(.green)
GeometryReader { geometry in
VStack {
EmptyView()
}.preference(key: WidthPreferenceKey.self, value: [geometry.size.width])
}
.frame(height: 0)
ForEach(self.items()) { item in
Text(item.value)
.frame(width: self.width / CGFloat(2), height: self.width / CGFloat(2))
.background(Color.red)
}
}
.padding()
}
.onPreferenceChange(WidthPreferenceKey.self) { value in
self.width = value[0]
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Is this the only way of doing that, or is there a more elegant and easier way to do that?
First of all, since you are using a VStack, means you need a vertical scrolling. So, you need to set maxHeight property, instead of maxWidth.
In your first approach, you are wrapping your for..loop in a GeometryReader before wrapping it in a VStack. So, SwiftUI doesn't recognize that you want those items in a vertical stack until before building GeometryReader and its children. So, a good workaround is to put GeometryReader as the very first block before your scrollView to make it work:
import SwiftUI
struct Item: Identifiable {
var id = UUID().uuidString
var value: String
}
struct ContentView: View {
func items() -> [Item] {
var items = [Item]()
for i in 0..<100 {
items.append(Item(value: "Item #\(i)"))
}
return items
}
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
Image(systemName: "circle.fill")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.padding()
.foregroundColor(.green)
ForEach(self.items()) { item in
Text(item.value)
.frame(width: geometry.size.width / CGFloat(2), height: geometry.size.width / CGFloat(2))
.background(Color.red)
}
}
.frame(maxHeight: .infinity)
.padding()
}
}
.background(Color.blue)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The issue with your second approach, is that you are wrapping your infinite-height VStack in a GeometryReader, which is not necessarily has an infinite height. Besides, GeometryReader is a SwiftUI block to read the dimensions from outer block, rather than getting dimensions from its children.
Generally speaking, if you want to use Geometry of current main SwiftUI component which you are making (or the device screen, if it's a fullscreen component), then put GeometryReader at a parent level of other components. Otherwise, you need to put the GeometryReader component as the unique child of a well-defined dimensions SwiftUI block.
Edited: to add padding, simply add required padding to your scrollView:
ScrollView{
//...
}
.padding([.leading,.trailing],geometry.size.width / 8)
or in just one direction:
ScrollView{
//...
}
.padding(leading,geometry.size.width / 8) // or any other value, e.g. : 30
If you need padding just for items in for loop, simply put the for loop in another VStack and add above padding there.
I had a layout that essentially looked like this:
ZStack(alignment: .bottom) {
GeometryReader { geometry in
ZStack {
Text("Centered")
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
.background(Color.red)
}
Group {
GeometryReader { geometry in // This GeometryReader is causing issues.
VStack {
Text("I want this at the bottom")
}
.frame(width: geometry.size.width, height: nil, alignment: .topLeading)
}
}
}
When this is rendered, both Text elements are rendered in the center of the screen. The second text element's container takes up the entire width of the screen, which is intended. If I remove the problematic GeometryReader, then the text is properly rendered at the bottom of the screen, but obviously the frame is not set to the entire width of the screen. Why is this happening?
By default SwiftUI containers tight to content, but GeometryReader consumes maximum of available space. So if to remove second GeometryReader the VStack just wraps internal Text.
If it is still needed to keep second GeometryReader (to read width) and put text to the bottom, the simplest approach would be to add Spacer as below
Group {
GeometryReader { geometry in
VStack {
Spacer()
Text("I want this at the bottom")
}
.frame(width: geometry.size.width, height: nil, alignment: .topLeading)
}
}
Alternate approach of how to stick view at bottom you can find in my answer in this post Position view bottom without using a spacer
How about this?
struct ContentView: View {
var body: some View {
ZStack(alignment: .bottom) {
GeometryReader {geometry in
Text("Centered")
.frame(width: geometry.size.width, height: geometry.size.height)
.background(Color.red)
}
WidthReader {w in
Text("I want this at the bottom").frame(width: w)
}
}
}
}
struct WidthReader<Content: View>: View {
let widthContent: (CGFloat) -> Content
#State private var width: CGFloat = 0
#State private var height: CGFloat = 0
var body: some View {
GeometryReader {g in
widthContent(width).background(
GeometryReader {g1 in
Spacer().onAppear {height = g1.size.height}.onChange(of: g1.size.height, perform: {height = $0})
}
).onAppear {width = g.size.width}.onChange(of: g.size.width, perform: {width = $0})
}.frame(height: height)
}
}
The easiest way is to add the .fixedSize() modifier to your Stack.