Unexpected layout issues with SwiftUI Image and ZStack - swift

I'm trying to create a view composed of two parts - the top part is an image with a piece of text overlaid in the top left corner. The top part should take up 2/3 of the height of the view. The bottom part is text information and a button, contained in an HStack, and takes up the remaining 1/3 of the view height.
struct MainView: View {
var body: some View {
GeometryReader { gr in
VStack(spacing: 0) {
ImageInfoView()
.frame(height: gr.size.height * 0.66)
BottomInfoView()
}
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}
}
struct ImageInfoView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Image("testImage")
.resizable()
.aspectRatio(contentMode: .fill)
.overlay(Rectangle()
.fill(Color.black.opacity(0.5))
)
HStack {
Text("Top left text")
.foregroundColor(.white)
.padding()
}
}
}
}
struct BottomInfoView: View {
var body: some View {
HStack {
VStack(alignment:.leading) {
Text("Some title")
Text("Some subtitle")
}
Spacer()
Button("BUTTON") {
}
}
.padding()
.background(.gray)
}
}
In the below sample code, if I set the frame height to 400 I will see the top left text, but when I set it to 200, I do not. What is happening here? The text should be anchored to the top left corner of ImageInfoView no matter what. Further, when set to 400, why does the ImageInfoView take up more than 66% of the height?
struct TestView: View {
var body: some View {
MainView()
.frame(height: 200)
.padding([.leading, .trailing], 16)
}
}

An image's fill pushes ZStack boundaries, so Text goes off screen (see this for details).
A possible solution is to put image into overlay of other container so it respects provided space instead of imposing own.
Tested with Xcode 13.4 / iOS 15.5
ZStack(alignment: .topLeading) {
Color.clear.overlay( // << here !!
Image("testImage")
.resizable()
.aspectRatio(contentMode: .fill)
.overlay(Rectangle()
.fill(Color.black.opacity(0.5))
)
)
// ... other code as-is

This fixes the problem:
ZStack(alignment: .topLeading) {
GeometryReader { gr in
Image("testImage")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: gr.size.width, maxHeight: gr.size.height)
.overlay(Rectangle()
.fill(Color.black.opacity(0.5))
)
HStack {
Text("Top left text")
.foregroundColor(.white)
.padding()
}
}
}
}

Related

Gab being caused by unknown element in swift design

Ok I have tried previous suggestions of just placing it in another vstack but still my view is showing the rectangle at the mid of the screen is their any reason why, also the edges have no padding around them making it edge to edge which I don't want?
struct ProfileView : View
{
var body: some View {
GeometryReader{ geo in
VStack {
VStack(alignment: .trailing, spacing: 10) {
RoundedRectangle(cornerRadius: 20)
.stroke(Color.primary, lineWidth: 2)
.frame(width: geo.size.width, height: 200)
.padding(.horizontal)
}.frame(width: geo.size.width, height: geo.size.height) // <<=== Here }
}
}
}
My Views are being injected into the content area via this page
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
TabView{
HomeView().tabItem {
Image(systemName: "house")
Text("Home")
}
ProfileView()
.tabItem {
Image(systemName: "person.circle.fill")
Text("Profile")
}
ZStack {
StatsView()
.tabItem {
Image(systemName: "murry").renderingMode(.original).padding()
Text("Plus")
}
}
StatsView()
.tabItem {
Image(systemName: "dumbbell.fill").renderingMode(.original).padding()
Text("Stats")
}
StatsView()
.tabItem {
Image(systemName: "dumbbell.fill").renderingMode(.original).padding()
Text("Notes")
}
}
}.safeAreaInset(edge: .bottom, alignment: .center, spacing: 0) {
Color.clear
.frame(height: 20)
.background(Material.bar)
}
}}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewInterfaceOrientation(.landscapeLeft)
}
}
On Simlualtor and real device its placing it in the middle of the screen.
I want this frame so that I can place other elements in side it is a rounded rectangle the best way to go for this?
You are using GeometryReader as the outer most view in the ProfileView, and GeometryReader has the behavior to take as much space as there is available, if you want your rectangle to show in the top or bottom of the screen consider using a Spacer() and if you want to add padding to your rectangle so it does not go to the edges of the screen try removing the .frame(width: geo.size.width) because you are forcing the rectangle to be the width of the screen, in nature the rectangle will take as much space as it can so you do not have to specify that.
This is your code but with some tweaks:
struct ProfileView : View {
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 20)
.stroke(Color.primary, lineWidth: 2)
.frame(height: 200)
.padding(.horizontal)
Spacer()
}
}
}

SwiftUI Image with high height layout

I'm trying to get a basic thing, but I can't make it work!
I would like to have a VStack that contains a text an image and a second text.
Everything should be visible on the screen. So the Image should resize (crop top and bottom) to give spaces for the two texts... but not.
Without .scaledToFill() it's working well, but the image is stretched
The problem (I think) is because the image has a high height!
(I tried with GeometryReader, fixedSize, layoutPriority, but nothing that I tried works)
The image is from Wikipedia: Image link
struct TestView: View {
var body: some View {
VStack(spacing: 8) {
Text("Text 1")
Image("image")
.resizable()
.scaledToFill()
.padding()
Text("Text 2")
}.background(Color.blue)
}
}
Simulator screen
What is needed:
Image of what is needed
Thank you :)
Ok, I found a way
Color.clear
.background(Image("image")
.resizable()
.scaledToFill())
.clipped()
Embedding the image as background of a clear Color, the image is limited to the parent size!
Result: https://imgur.com/a/ceZRqSw
struct TestView: View {
var body: some View {
VStack(spacing: 8) {
Text("Text 1")
Image("image")
.resizable()
.scaledToFill()
.padding()
Text("Text 2")
}
.background(Color.blue)
.padding(.top)
}
}
You could also use the maxHeight frame parameter to frame the view if you know how big you'd want it to be. You can also do this on the image itself.
struct TestView: View {
var body: some View {
VStack(spacing: 8) {
Text("Text 1")
Image("image")
.resizable()
.scaledToFill()
.padding()
Text("Text 2")
}
.background(Color.blue)
.frame(maxHeight: 500) //500 can be any value of your choice
}
}
You can also use the AsyncImage with placeholder to load the image
struct ContentView: View {
var body: some View {
VStack(spacing: 8) {
Text("Text 1")
AsyncImage(url: URL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ee/Isabella_of_France_by_Froissart.png/1024px-Isabella_of_France_by_Froissart.png")){ image in
image.resizable()
}placeholder: {
ProgressView()
}
.scaledToFill()
.frame(width: 200, height: 400, alignment: .center)
Text("Text 2")
}
.background(Color.blue)
}
}
It seems like you are looking for a way to consider Safe Area. because at the second image all contents respect to safe area while the first image don't. However the code seems to do so by default unless we indicate to ignore safe area by using .ignoresSafeArea(). Look at the view where you called TextView, You might use ZStack with a blue background that affects its children.
If not you should used scaledToFit instead of scaledToFill because as I see both images, the second one is cropped.
Have you tried
struct TestView: View {
var body: some View {
VStack(spacing: 8) { // You can alternatively increase the spacing and remove the padding for the image
Text("Text 1")
Image("image")
.resizable()
.scaledToFill()
.padding()
Text("Text 2")
}.background(Color.blue)
.padding() // Note padding here
}
}

Spacer() swiftui leaves a white space

I'm currently building a UI that has a big red 250px height box on the top that holds information and a scrollview underneath it. I build this all in a VStack(). I put a spacer() at the bottom of the VStack to push everything to the top of the screen. But it has a big white space between the red container at the top and the scrollview. Even though both don't have any padding(). Does anyone know how this is possible? This is my code:
import SwiftUI
struct User: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
NavigationView {
VStack {
...
code
...
}
.frame(width: UIScreen.main.bounds.width, height: 250)
.background(Color.red)
.ignoresSafeArea()
// somehow it has a white space here...
ScrollView {
LazyVGrid(columns: columns, spacing: 2) {
ForEach((0...56), id: \.self) {_ in
Image("testimage")
.resizable()
.scaleEffect(CGSize(width: 1.0, height: 2.0))
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
.cornerRadius(10)
}.padding(.bottom, 75)
}
.frame(width: UIScreen.main.bounds.width)
.padding(.top, -10)
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
// this Spacer should push everything to the top
Spacer()
}
}
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
Edit:
To make my question better understandable I made this summery:
I have build a VStack with 2 items in it. there is a big white space between the two and I don't know how to get rid of this white space. If I remove .ignoresSafeArea() the white space disappears. The thing is I need .ignoresSafeArea() to push the first item in VStack up. Does anyone know what I can do to keep .ignoresSafeArea() but get rid of the white space?
I added a screenshot of the problem. I gave the scrollview a green background so that the white spacing between the VStack and the Scrollview is visable.
You need to embed everything in a VStack with 0 spacing.
import SwiftUI
struct User: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
NavigationView {
VStack(spacing: 0) {
VStack {
Text("Hello World!") //Easier to test
}
.frame(height: 250) //Dont use UIScreen
.background(Color.red)
.ignoresSafeArea()
// somehow it has a white space here...
ScrollView {
LazyVGrid(columns: columns, spacing: 2) {
ForEach((0...56), id: \.self) {_ in
Image("testimage")
.resizable()
.scaleEffect(CGSize(width: 1.0, height: 2.0))
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
.cornerRadius(10)
}.padding(.bottom, 75)
}
.frame(width: UIScreen.main.bounds.width)
.padding(.top, -10)
}
Spacer()
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
}
Also, you might need to give the ScrollView a max height to push it upwards. .frame(maxHeight: 175)
As you haven't given a reproducible example or an image. I cant tell you for certain what will work
I guess Arnavs Answer is correct.
Notice:
When you are trying to implement multiple Views on one page, 90% of the time it is better to have them in one H/V/Z-Stack.
For a better understanding start a new project and try ...
Navigationview{
VStack{
Text("Hello Vertical World")
Text("Hello Vertical World2")
}
HStack{
Text("Hello Horizontal World")
Text("Hello Horizontal World2")
}
}
and the same thing but "different"
NavigationView {
VStack{
Text("Hello Vertical World")
Text("Hello Vertical World2")
HStack{
Text("Hello Horizontal World")
Text("Hello Horizontal World2")
}
}
}
I can't double check bc i am on my phone, but this should look very different.
I fixed the issue by putting the VStack and the Scrollview in an other VStack in the navigationView. Then I put the .ignoresSafeArea() on this new VStack.
Code:
import SwiftUI
struct User: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
NavigationView {
// new VStack that includes the topbar VStack, the
// scrollview and has the .ignoresSafeArea() on it.
VStack {
VStack {
...
Code topbar
...
}
.frame(width: UIScreen.main.bounds.width, height: 250)
.background(Color.red)
Scrollview {
...
Code scrollview
...
}
Spacer()
}
.ignoresSafeArea()
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}

Geometryreader affecting scrollview

When adding a standard Scrollview I can scroll to the end of my list without any issues, it's after adding a GeometryReader on my button that the scroll list doesn't scroll to the end of the list but stops somewhere at the top of my button. The geometryReader does not scale to the size of the child object, how can I change this without specifying a hard coded frame size for the GeometryReader ? I'm using the GeometryReader to later identify the top of my button corresponding the position on the screen with geo.frame(in: .global).maxY
struct scroll: View {
private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]
var body: some View {
ScrollView {
VStack {
LazyVGrid(columns: gridItemLayout) {
Circle()
.scaledToFill()
.foregroundColor(.red)
Circle()
.scaledToFill()
.foregroundColor(.green)
Circle()
.scaledToFill()
.foregroundColor(.blue)
Circle()
.scaledToFill()
.foregroundColor(.yellow)
}
GeometryReader { geo in
Button(action: {
}) {
Text("Adjust time")
}
.cornerRadius(18.0)
.padding(.top, 5)
}
//.frame(width: .infinity, height: 50)
}
}
}
}
When checking the height and trying to set the frame of the GeometryReader equal to the height of the button, the button it also gives me 0 as value. When I try to read the width of the button with geo.frame.width, it gives me a correct value.?
struct scroll: View {
private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]
#State private var buttonHeight: CGFloat = .zero
var body: some View {
ScrollView {
VStack {
Text("\(buttonHeight)")
LazyVGrid(columns: gridItemLayout) {
Circle()
.scaledToFill()
.foregroundColor(.red)
Circle()
.scaledToFill()
.foregroundColor(.green)
Circle()
.scaledToFill()
.foregroundColor(.blue)
Circle()
.scaledToFill()
.foregroundColor(.yellow)
}
GeometryReader { geo in
Button(action: {
}) {
Text("Adjust time")
}
.onAppear(perform: {
buttonHeight = geo.size.height
})
.cornerRadius(18.0)
.padding(.top, 5)
}
.frame(width: .infinity, height: buttonHeight)
.background(Color.gray)
}
}
}
}
You can read it by putting GeometryReader into background of button, like
Button(action: {
}) {
Text("Adjust time")
}
.cornerRadius(18.0)
.padding(.top, 5)
.background(GeometryReader { geo in
// geo.frame(in: .global).maxY // << global position of button
})
You can then even store it in view preference to pass into state, like in https://stackoverflow.com/a/62466397/12299030.

SwiftUI: Prevent Image() from expanding view rect outside of screen bounds

What I'm trying to achieve
I'm trying to create a SwiftUI view where an image should expand the entire screen (edgesIgnoringSafeArea(.all)), and then overlay a view on top of that, that also fills the entire screen, but respects the safe area.
What I've tried
This is my code, which comes close:
struct Overlay: View {
var body: some View {
VStack {
HStack {
EmptyView()
Spacer()
Text("My top/right aligned view.")
.padding()
.background(Color.red)
}
Spacer()
HStack {
Text("My bottom view")
.padding()
.background(Color.pink)
}
}
}
}
struct Overlay_Previews: PreviewProvider {
static var previews: some View {
ZStack {
Image(uiImage: UIImage(named: "background")!)
.resizable()
.edgesIgnoringSafeArea(.all)
.aspectRatio(contentMode: .fill)
Overlay()
}
}
}
The issue and tested solutions
The issue is that the image is not clipped it looks like, so it expands the parent view to a width larger than the screen width, which then makes the top right aligned red text box float off screen (see image).
I tried using .clipped() in various places, with no luck. I would preferably avoid using GeometryReader if possible.
Q: How can I make the image view only fill the screen?
You have to limit the frame size of the out-of-bounds Image before it is being picked up by the ZStack to avoid the ZStack to grow and so the Overlay to go out of position.
edit: aheze shows with his answer a way around using GeometryReader by putting the Image into the background of Overlay() with .background(Image()..). This avoids the usage of ZStack and GeometryReader completely and is possibly a cleaner solution.
Based on parent view size
struct IgnoringEdgeInsetsView2: View {
var body: some View {
ZStack {
GeometryReader { geometry in
Image("smile")
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.frame(maxWidth: geometry.size.width,
maxHeight: geometry.size.height)
}
Overlay()
}
}
}
Based on screen size
struct IgnoringEdgeInsetsView: View {
var body: some View {
ZStack {
Image("smile-photo")
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.frame(maxWidth: UIScreen.main.bounds.width,
maxHeight: UIScreen.main.bounds.height)
Overlay()
}
}
}
No need to mess with GeometryReader. Instead, you can prevent the image from overflowing by using the .background() modifier.
struct ContentView: View {
var body: some View {
Overlay()
.background( /// here!
Image("City")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea()
)
}
}
Result: