Resize image to screen width using `Vstack` while maintaining aspect ratio - swift

I am working with VStack to show an image (they will be several with different sizes from a json)
I need to show it to occupy the screen width (the width of the vstack and maintaining aspect ratio) and to be properly resized, respecting the height according to the width of the screen.
I have tried in different ways, but I manage to display the image correctly.
My View is:
struct ContentView: View {
var body: some View {
VStack {
GeometryReader { geometry in
VStack {
Text("Width: \(geometry.size.width)")
Text("Height: \(geometry.size.height)")
}
.foregroundColor(.white)
}
.padding()
.frame(alignment: .topLeading)
.foregroundColor(Color.white) .background(RoundedRectangle(cornerRadius: 10) .foregroundColor(.blue))
.padding()
GeometryReader { geometry in
VStack {
Image("sample")
.resizable()
//.frame(width: geometry.size.width)
.aspectRatio(contentMode: .fit)
}
.foregroundColor(.white)
}
.frame(alignment: .topLeading)
.foregroundColor(Color.white) .background(RoundedRectangle(cornerRadius: 10) .foregroundColor(.blue))
.padding()
}
.font(.title)
}
}
When I use .frame (width: geometry.size.width) by assigning the width of the geometry, the width is shown to the entire screen but the height is not maintaining aspect ratio. (looks crushed)
How can I get the dimensions of the image and find its proportion to use it with .aspectRatio (myratio, contentMode: .fit)
There is another way to display the image correctly, any suggestions

You need to eliminate the second GeometryReader, because having two children in the VStack that both accept as much space as they are offered is going to make the VStack unable to give the Image the correct amount of space.
You also need to raise the layout priority of the Image so that the VStack offers it space first, so it can take as much as it needs.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
GeometryReader { geometry in
VStack {
Text("Width: \(geometry.size.width)")
Text("Height: \(geometry.size.height)")
}.foregroundColor(.white)
}.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.blue))
.padding()
Image(uiImage: UIImage(named: "sample")!)
.resizable()
.aspectRatio(contentMode: .fit)
.layoutPriority(1)
}
}
}
import PlaygroundSupport
let host = UIHostingController(rootView: ContentView())
host.preferredContentSize = .init(width: 414, height: 896)
PlaygroundPage.current.liveView = host
Result:

Related

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

Adjusting big or small screens using geometry reader

I am currently stuck in trouble i am not really able to resize image and texts to fit bigger or small screens. This is just a simple card that i want to be able to be resizable and should be in the same proportions. I have even tried geometry reader. I am having trouble as even if I have used geometry reader reader the white spaces I wanted are not there as the picture takes the entire width and height which looks bad not ideal. If there is a way for me to have the same proportion of the card for all different screen sizes then I am all open to ideas.
Image of my card:
Here is my code:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Image("shops")
.resizable()
.aspectRatio(contentMode: .fill)
VStack {
Spacer()
Text("Welcome to MarketInfo")
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.white)
}
.padding(.bottom, 30)
.frame(width: 400)
}
.frame(width: 380, height: 270)
.cornerRadius(20)
.clipped()
.shadow(radius: 8)
.padding(.top, 20)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
I think where you are running into issues is in using the fixed frame. The issue with using it is that it does not adjust for sizes. If you had something complicated, you could use a GeometryReader to resize the frame based on a proportion of the available space. However, the simplest implementation is setting your Text as an .overlay() on your image and setting the .aspectRatio(contentMode: .fit).
The code would now be:
struct ContentView: View {
var body: some View {
Image("shops")
.resizable()
.aspectRatio(contentMode: .fit)
.overlay(
VStack {
Spacer()
Text("Welcome to MarketInfo")
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.white)
}
)
.cornerRadius(20)
.clipped()
.shadow(radius: 8)
.padding(30)
}
}
The overlay is never bigger than the Image, which can be an issue using a ZStack, and it will move with the Image. I then just through some padding on the Image all around to keep it in from the sides, and away from other views.
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack() {
Image("shops")
.resizable()
.aspectRatio(1.4, contentMode: .fit) // 380 / 270 = 1.4
.overlay(
VStack {
Spacer()
Text("Welcome to MarketInfo")
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.white)
.lineLimit(1)
.minimumScaleFactor(0.5)
.padding()
})
}
// .frame is not required, but Change .infinity to a number will resize the card
.frame(maxWidth: .infinity)
.cornerRadius(20)
.shadow(radius: 8)
.padding(.horizontal, 20)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPhone 13 Pro")
ContentView()
.previewDevice("iPod touch (7th generation)")
ContentView()
.previewDevice("iPad Pro (12.9-inch) (5th generation)")
}
}

Align heights of HStack members

I want to align the heights of two HStack members. The expected outcome would be that the image has the same size as the text. The current outcome is that the image has more height than the text. This is my current setup:
HStack {
Text("Sample")
.font(.largeTitle)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.red)
)
Spacer()
Image(systemName: "checkmark.seal.fill")
.resizable()
.scaledToFit()
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.red)
)
}
What I've tried:
.fixedSize() -> I tried to tack this modifier onto the Image but the result was that the Image's height got smaller than the text's. Probably because the SFSymbol's intrinsic height is smaller than the .largeTitle intrinsic height.
AlignmentGuide -> I tried to create a custom alignment guide where I initially thought I could say "align bottom of Image and Text and align top of Image and Text" and therefore have the same height. But it seemed like you can only apply a single alignment guide per stack view.
GeometryReader -> I tried to wrap the HStack in a GeometryReader in which I tacked the .frame(height: proxy.frame.height) view modifier on the Text and Image. This also did not help because it somehow just made some white space around the views.
How it is:
How I want it:
Wrap your Image in a Text. Since your image is from SF Symbols, SwiftUI will scale it to match the dynamic type size. (I'm not sure how it will scale other images.)
VStack {
let background = RoundedRectangle(cornerRadius: 10)
.foregroundColor(.red)
ForEach(Font.TextStyle.allCases, id: \.self) { style in
HStack {
Text("\(style)" as String)
.padding()
.background(background)
Spacer()
Text(Image(systemName: "checkmark.seal.fill"))
.padding()
.background(background)
}
.font(.system(style))
}
}
You can get the size of the Image small by adding a .frame() modifier to your HStack. See the code below,
HStack {
// Some Content
}
.frame(height: 60) // Interchangeable with frame(maxHeight: 60)
The Result:
For your exact example, I found 60 to be the sweet spot. But if you wanted a more dynamic solution, I'd make a few changes to your code. See the code below.
HStack {
Text("Sample")
.font(.largeTitle)
.frame(maxHeight: .infinity) // force the text to take whatever height given to the Parent View, which is the HStack
.padding(.horizontal) // Add padding to the Text to the horizontal axis
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.red)
)
Spacer()
Image(systemName: "checkmark.seal.fill")
.resizable()
.scaledToFit()
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.red)
)
}
.background(Color.gray)
.frame(height: 100) // Change this value and the embedded Views will fit dynamically
The output will work as shown in the GIF below,
Here is an upgrade version of rob answer which support Assets Image plus system Image! Almost any Image! Like this:
struct ContentView: View {
var body: some View {
VStack {
ForEach(Font.TextStyle.allCases, id: \.self) { style in
HStack {
Text(String(describing: style))
.padding()
.background(Color.pink.opacity(0.5).cornerRadius(10.0))
Spacer()
}
.font(.system(style))
.background(
Image("swiftPunk")
.resizable()
.scaledToFit()
.padding()
.background(Color.yellow.cornerRadius(10.0))
, alignment: .trailing)
}
}
}
}
Result:

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:

HStack with SF Symbols Image not aligned centered

I have this simple SwiftUI code. I want all symbols to be aligned centered, just like the cloud symbol.
struct ContentView : View {
var body: some View {
HStack(alignment: .center, spacing: 10.0) {
Image(systemName: "cloud.sun")
Image(systemName: "cloud")
Image(systemName: "cloud.bolt")
Text("Text")
}.font(.title)
}
}
But as you can see below, the first and the last symbol are not centered. Am I missing something, or is this a bug?
Cheers!
This is what it's going on.
The Image views are not resizing.
It looks like they're not aware of their intrinsic content size, or maybe it reports the wrong value.
To fix it:
struct ContentView : View {
var body: some View {
HStack(alignment: .center, spacing: 10.0) {
Image(systemName: "cloud.sun")
.resizable()
.aspectRatio(contentMode: .fit)
.background(Color.red)
Image(systemName: "cloud")
.resizable()
.aspectRatio(contentMode: .fit)
.background(Color.yellow)
Image(systemName: "cloud.bolt")
.resizable()
.aspectRatio(contentMode: .fit)
.background(Color.pink)
Text("Text").background(Color.green)
}
.frame(width: 250, height: 50)
.background(Color.gray)
.font(.title)
}
}
...make the Images resizable, and also make sure the aspect ratio is set to .fit, or they will stretch.
Set also frame on the HStack or it will expand to fill the whole screen.
#MartinR suggested an even better solution - creating the images via UIImage - see his comment below.
struct ContentView : View {
var body: some View {
HStack {
Image(uiImage: UIImage(systemName: "cloud.sun")!)
.background(Color.red)
Image(uiImage: UIImage(systemName: "cloud")!)
.background(Color.yellow)
Image(uiImage: UIImage(systemName: "cloud.bolt")!)
.background(Color.pink)
Text("Text").background(Color.green)
}
.background(Color.gray)
.font(.title)
}
}
Output:
I came across the same problem as you: SF Symbols are not reporting the correct content size on iOS 13. (Though, it is fixed on iOS 14 and above.)
The problem with using UIImage as proposed in Matteo Pacinis solution is the poor compatibility with the SwiftUI dynamics: foreground color and font size (and dynamic type!) are not simply taken from the current SwiftUI context but have to be duplicated into the UIImage configuration (using UIImage(systemName:, withConfiguration:)) wich is often not practical. (see hackingwithswift.com for how to use this).
Looking at the problem above I propose the following solution:
HStack(alignment: .center, spacing: 10.0) {
Image(systemName: "cloud.sun")
.hidden()
.overlay(
Image(systemName: "cloud.sun")
.resizable()
.aspectRatio(contentMode: .fill)
.background(Color.red)
)
Image(systemName: "cloud")
.hidden()
.overlay(
Image(systemName: "cloud")
.resizable()
.aspectRatio(contentMode: .fill)
.background(Color.yellow)
)
Image(systemName: "cloud.bolt")
.hidden()
.overlay(
Image(systemName: "cloud.bolt")
.resizable()
.aspectRatio(contentMode: .fill)
.background(Color.pink)
)
Text("Text")
.background(Color.green)
}
.background(Color.gray)
.font(.title)
It looks like a lot of code duplication but it has the advantage, that the symbols scale according to your font, respect the foregroundColor modifier and align according to your desired alignment
Output:
But there is still some issue with this approach: the image still has not the correct intrinsic size, the symbols are simply drawn "nicely centered". This means that the height of the HStack still depends of the height of the Text element.
If you simply want to draw the symbols in .largeTitle font and the text in .body font, the result would look like below possibly causing overlapping with neighbouring views:
I will still investigate further to find a solution ensuring the correct view size, as this really annoys me.