SwiftUI: Dynamically set size for Image - ios

I want to show an image in a View, where the image source will vary in size.
Is there a way to dynamically adjust the size of the image to the width of the containing view or get the screen size even?
I have a horizontal scrollview with each element being a Stack of an image and a text. I would like to size every image depending on the screen width, since I want to be independent of the device.
How would you do that in SwiftUI?
Thanks!
struct ImageRow: View {
var items: [ImageWithDescription]
var body: some View {
ScrollView(showsHorizontalIndicator: false) {
HStack(alignment: .bottom){
ForEach(self.items.identified(by: \.name)) { item in
Spacer()
VStack {
Image(uiImage: item.image ?? UIImage())
.resizable()
.frame(minWidth: (item.image.size.width) / 3, height: (item.image.size.height) / 3)
.cornerRadius(20, antialiased: true)
.clipShape(Rectangle())
.shadow(radius: 10)
Text(item.name)
.color(.primary)
.font(.subheadline)
}
}
.padding()
}
}
.frame(height: 300)
}
}

You can get the size of the screen (or window, in case you are using an iPad with split screen).
In the example below, the top view uses GeometryReader to get the size and passes it down the hierarchy. The subview uses that data to create a blue rectangle with half the size of the screen. It will also adapt if the device is rotated.
GeometryReader has many features, and although they are barely documented, I've written an article which fully explains how to use them: https://swiftui-lab.com/geometryreader-to-the-rescue/
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
MySubview(size: geometry.size)
}
}
}
struct MySubview: View {
let size: CGSize
var body: some View {
print("size = \(size)")
return Color.blue.frame(width: size.width / 2, height: size.height / 2).fixedSize()
}
}

Related

Unable to remove space between two Horizontal Scroll views in SwiftUI?

I am trying to create two horizontal scrollable list of elements. Currently, this is what I am able to achieve:
As can be seen, I want to remove the space between the two Scroll Views. Here, is the code I am using to achieve this:
var body: some View {
NavigationView {
VStack (spacing: 0){
Text("Zara") .font(.custom(
"AmericanTypewriter",
fixedSize: 36))
ScrollView (.horizontal){
HorizontalScrollModel(searchResult: searchResults1)
}.task {
await loadData()
}
ScrollView (.horizontal){
HorizontalScrollModel(searchResult: searchResults1)
}
}
}
}
HorizontalScrollModel() returns a LazyHGrid of the products. I have tried setting the spacing to 0 with VStack (spacing: 0) but this did not work. How do I remove the space between the ScrollViews?
HorizontalScrollModel:
struct HorizontalScrollModel: View {
var searchResults1: [homeResult]
let columns = [
GridItem(.adaptive(minimum: 200))]
#State private var isHearted = false
init( searchResult: [homeResult]) {
self.searchResults1 = searchResult
}
var body: some View {
LazyHGrid (rows: columns ){
ForEach(searchResults1, id: \.prodlink) { item in
NavigationLink{
ProductView(imageLink: item.image_src, productLink: item.prodlink ,
productCost: String(item.price))
} label: {
VStack (alignment: .leading, spacing: 0){
AsyncImage(url: URL(string: item.image_src)) { phase in
if let image = phase.image {
image
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 10))
} else if phase.error != nil {
Text("There was an error loading the image.")
} else {
ProgressView()
}
}
.padding()
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.gray.opacity(0.2), lineWidth: 4)
)
.padding(.top, 2)
.padding(.bottom, 2)
.padding(.leading, 2)
.padding(.trailing, 2)
.overlay (
RoundedRectangle(cornerRadius: 12)
.stroke(.gray.opacity(0.3), lineWidth: 1))
}
}
}
// .padding([.horizontal, .bottom])
}
}
}
Each ScrollView takes all available space offered by VStack, which since it has 2 views will be half the screen for each view.
Inside each ScrollView you then place a LazyHGrid, and inside the cell you are placing a 150x150 image, but the ScrollView size will not change to fit the image.
So, it now depends on what you're aiming for. If you just want the two ScrollView to be joined together, you can have a .frame modified to the outer VStack with a height of 308 (150 for each image + the padding).
Try adding the .fixedSize() modifier to the ScrollView:
ScrollView(.horizontal) {
HorizontalScrollModel(searchResult: searchResults1)
}
.fixedSize(horizontal: false, vertical: true)
You might also be able to add it just to the HorizontalScrollModel.

How to make `VStack` view inside `ScrollView` expand up and take the space under navigation title while keeping the content in the same spot?

Here is an example of what i currenly have:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack() {
Text("Foo")
}
.frame(maxWidth: .infinity, idealHeight: 200)
.background(Color(uiColor: .systemBackground))
}
.background(Color(uiColor: .secondarySystemBackground))
.navigationTitle("Title")
}
}
}
Here is what it renders:
What i want to achieve is the white background all the way up to the top of the screen while preserving the padding so that the actual content of the VStack is displayed below the title and not underneath it and when scroll happens VStack's content follows the title when it goes from large to inline.
Alright, i found a way to do that. It's hacky and most probably won't work properly for all screen sizes or when changing orientation of the device but i recon it would work for most iPhone models in portrait mode.
The idea is to place a ZStack inside ScrollView and put my VStack there along with a Rectangle of required color and then offset the Rectangle up to occupy the space underneath the navigation bar and more to keep the color the same when scroll spring effect happens.
Here it the code:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
ZStack(alignment: .top) {
Rectangle()
.foregroundColor(Color(uiColor: .systemBackground))
.frame(height: UIScreen.main.bounds.height * 0.5)
.offset(y: -UIScreen.main.bounds.height * 0.5)
.padding(.bottom, -UIScreen.main.bounds.height * 0.5)
VStack() {
Text("Foo")
}
.frame(maxWidth: .infinity)
.frame(height: 200)
.background(Color(uiColor: .systemBackground))
}
}
.background(Color(uiColor: .secondarySystemBackground))
.navigationTitle("Title")
}
}
}
Note: don't forget to set alignment: .top to ZStack!

onDrag preview isn’t just of the dragged view but also displays the background the view is laid on top of

I’m using an .onDrag modifier on a view that has rounded corners:
struct RootView: View {
#State var dragging: Bool = false
var body: some View {
VStack {
Color.red.cornerRadius(32)
.frame(width: 300, height: 300)
.onDrag {
dragging = true
return NSItemProvider(object: NSString())
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.green)
}
}
The problem is when I invoke a drag - due to the automatic drop shadow effect applied by the system - it doesn’t feel as though I’m dragging the card on its own. See below.
Is there any way to get the card to look dragged with a transparent background, as opposed to above whatever background colour it was laid on top of?
There is content shape type for this,
Color.red.cornerRadius(32)
.frame(width: 300, height: 300)
.contentShape(.dragPreview, RoundedRectangle(cornerRadius: 32)) // << here !!
.onDrag {

Constraint-based layout of images in a list in SwiftUI

I want to achieve the following constraint-based layout of images in a SwiftUI List:
pin left/right edges of each Image to the list margins (adapts to screen size)
dynamic height based on aspect ratio (I'd also be ok with fixed height)
maintain image aspect ratio, and content should fill the space
What I have tried and doesn't work (based on this article):
struct MyView: View {
#ObservedObject var viewModel: MyViewModel
let aspectRatio = CGSize(width: 345, height: 120)
var body: some View {
List {
ForEach(viewModel.items) { item in
GeometryReader { geo in
Image("test_image")
.resizable()
.aspectRatio(aspectRatio, contentMode: .fill)
.frame(width: geo.size.width)
.clipped()
}
}
}
}
}
The size I get from geo is (343, 32) on iPhone 11 Pro. Width makes sense but it's not letting the cells expand beyond a height of 32 for some reason. Any tips welcome because I'm really starting to miss auto layout constraints.
No need to use GeometryReader for something like this. For the fixed height, you can just supply a frame with height only. You also don't need to create your own let aspectRatio = CGSize(width: 345, height: 120) - if you leave it nil (by default) it should be fine.
Edit: Using padding instead of VStack with spacing
struct MyView: View {
var body: some View {
List {
ForEach(0..<10, id: \.self) { item in
Image("test_image")
.resizable()
.aspectRatio(contentMode: .fill) /// no need for custom aspect ratio
.frame(height: 120) /// fixed height of image
.clipped() /// stop image from overflowing
.padding(.vertical, 12) /// extra vertical padding
}
}
}
}
Result (with "test_image"):
However, this has a fixed height of 120, so the top and bottom of the images are cropped out. To fix this, you can just avoid frame and clipped altogether.
struct MyView: View {
var body: some View {
List {
ForEach(0..<10, id: \.self) { item in
Image("test_image")
.resizable()
.aspectRatio(contentMode: .fill) /// doesn't matter if it's fit or fill
.padding(.vertical, 12) /// extra vertical padding
}
}
}
}
Result:

How to center crop an image in SwiftUI

I'm new to SwiftUI. I guess everyone is at this point. I've been an app developer for about 6 years now and I feel stupid asking this question on StackOverflow. But I looked everywhere. How do I center crop an image in an ImageView in SwiftUI?
I know there's an option to change the aspect ratio but I only see fit and fill. I just want the imageView to centerCrop(android term) the image. Does anybody know?
Android's ImageView.ScaleType documentation describes CENTER_CROP as:
CENTER_CROP
Scale the image uniformly (maintain the image's aspect ratio) so that
both dimensions (width and height) of the image will be equal to or
larger than the corresponding dimension of the view (minus padding).
The image is then centered in the view.
This is essentially what Aspect Fill Scaling (aka .scaledToFill()) does, except (surprisingly) Aspect Fill doesn't clip the parts that fall outside of the frame.
By making the image .resizable, and applying .scaledToFill(). the image will be scaled proportionally to fill its available frame leaving off the top and bottom or sides as necessary. .clipped() then removes the parts of the image outside of the frame.
Image("myImage")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200, alignment: .center)
.clipped()
To make this more convenient, I created this extension of Image:
extension Image {
func centerCropped() -> some View {
GeometryReader { geo in
self
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
}
}
}
To use the Image extension, just put it in a file in your project (a name like image-centercropped.swift will work nicely). Then just add .centerCropped() to any image you want to be center cropped.
Image("apolloimage").centerCropped()
It uses GeometryReader to figure out its frame so that it can crop the image correctly, which means you don't have to specify the frame to get proper clipping. You are free to size the image however you like using an explicit frame, or by just adding padding() and Spacer() to keep it nicely placed relative to other user interface items.
For example: If you want an image to fill the screen of the phone:
struct ContentView: View {
var body: some View {
Image("apolloimage")
.centerCropped()
.edgesIgnoringSafeArea(.all)
}
}
will nicely show the center of the image by scaling the image to show either the full height or the full width of the image and cropping the parts the hang over on the other dimension.
Demonstration:
Here's a demo that shows how the image is centered and cropped as the image grows. In this demo, the frame width is a constant 360 while the frame height varies from 50 to 700 as the slider advances to the right. At the beginning when the frame is short, the tops and bottoms of the image are cropped. As the frame exceeds the aspectRatio of the original image, the resulting image is centered but cropped on the left and right sides.
struct ContentView: View {
#State private var frameheight: CGFloat = 50
var body: some View {
VStack(spacing: 20) {
Spacer()
Image("apolloimage")
.resizable()
.scaledToFill()
.frame(width: 360, height: self.frameheight)
.clipped()
Spacer()
Slider(value: self.$frameheight, in: 50...700)
.padding(.horizontal, 20)
}
}
}
or an equivalent test using .centerCropped():
struct ContentView: View {
#State private var frameheight: CGFloat = 50
var body: some View {
VStack(spacing: 20) {
Spacer()
Image("apolloimage")
.centerCropped()
.frame(width: 360, height: self.frameheight)
Spacer()
Slider(value: self.$frameheight, in: 50...700)
.padding(.horizontal, 20)
}
}
}
Alternate Solution
Another way to make a center cropped image is to make the image an .overlay() of Color.clear. This allows Color.clear to establish the clipping bounds.
Color.clear
.overlay(
Image("apolloimage")
.resizable()
.scaledToFill()
)
.clipped()
and the corresponding extension to Image looks like this:
extension Image {
func centerCropped() -> some View {
Color.clear
.overlay(
self
.resizable()
.scaledToFill()
)
.clipped()
}
}
I was able to crop the square centre of image for view like iPhone Photo app.
extension Image {
func centerSquareCropped() -> some View {
GeometryReader { geo in
let length = geo.size.width > geo.size.height ? geo.size.height : geo.size.width
self
.resizable()
.scaledToFill()
.frame(width: length, height: length, alignment: .center)
.clipped()
}
}
}
I was originally using #vacawama's answer for this which uses the GeometryReader but discovered that actually this isn't necessary.
(I'm writing this with Xcode 13 and running in iOS15 if that makes any difference?)
Using this is sufficient...
Image(uiImage: image) // insert your own image here :D
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
I'm using this as the label parameter of a Button inside a List so the whole thing is like...
Section("Photo") {
Button {
// the action
} label: {
if let image = viewStore.imagePickerState.image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
} else {
PersonAvatarButton()
}
}
}
.aspectRatio(1, contentMode: .fill)
.listRowBackground(Color.gray)
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
I haven't defined the frame anywhere for this, just the aspect ratio of the Section.
What I end up with is a square button with rounded corners and the photo all the way to the edge. Resized to fill but not squashed in any way. And the button size is determined by the screen size.
So I don't have any concrete sizes in my code.

Resources