iOS SwiftUI: prevent resizable() animation - ios

I have a cell with an image.
The image is downloaded from the internet, and I would like to have a fade opacity transition when the image is downloaded.
This is easily accomplished with the couple of modifiers:
.transition(.opacity)
.animation(.default)
But because my image has also the .resizable() modifier I also get a scaling animation that I really don't want.
How can I prevent this?
struct GridCellView: View {
#ObservedObject var model: CellViewModel
var body: some View {
GeometryReader { proxy in
Image(uiImage: self.model.image)
.resizable()
.transition(.opacity)
.animation(.default)
.scaledToFill()
.frame(width: proxy.size.width, height: proxy.size.width)
.aspectRatio(1/1, contentMode: .fit)
.clipped()
.animation(nil)
}
}
}

Use .animation(nil) right after property/ies which animations you want to disable, like below
var body: some View {
GeometryReader { proxy in
Image(uiImage: self.model.image)
.resizable()
.animation(nil) // << disables animation
.transition(.opacity)
.animation(.default) // << enables animation
.scaledToFill()
.frame(width: proxy.size.width, height: proxy.size.width)
.aspectRatio(1/1, contentMode: .fit)
.clipped()
}
}

I would add a #State private var isDownloaded = false to your struct. Then when the download is finished use the ternary operator to animate the opacity. Something like this.
Image(uiImage: self.model.image)
.resizable()
.opacity(isDownloaded ? 1 : 0)
.animation(.default)

Related

SwiftUI Tap Area Below Button

I have a View in my app that shows an Image as the background (via Binding) and then it has a button to download the image and another to favorite the image.
The problem is that when I use the .ignoreSafeArea on a whole ZStack, the tap area on the button shifts to below the button body. I have the images scaled to fit and clipped and I tried moving the buttons down with padding, and I have tried seeing if there was an alternative to making the whole ZStack ignore the safe area with no luck.
I should also mention that I have put the image as the background with the .background property, I tried just putting it at the top of a ZStack but that would also affect the buttons positions.
I should also mention I am relatively new to Swift
struct photoDetail: View {
let image: realPhoto
#State var textControl: Bool
init(image: realPhoto) {
self.image = image
self.textControl = image.favorited
}
var body: some View {
ZStack {
VStack(alignment: .leading) {
Spacer()
HStack {
Text("created by: \(image.username)")
.padding(5)
.foregroundColor(.white)
.background(Color(.gray).opacity(0.8))
.padding(.leading, 20)
.font(.system(size:20, weight: .semibold))
.padding(.bottom, 50)
.cornerRadius(4)
Spacer()
}.padding(.bottom, 60)
}
VStack {
HStack {
Spacer()
Button {
let imageSaver = ImageSaver()
imageSaver.writeToPhotoAlbum(image: UIImage(data: image.image)!)
} label: {
Image(systemName: "square.and.arrow.down")
.resizable()
.scaledToFit()
.frame(width: 20)
.padding(8)
}
Button {
favoriteToggle()
} label: {
Image(systemName: textControl == false || image.favorited == false ? "heart": "heart.fill")
.resizable()
.scaledToFit()
.frame(width: 25)
.padding(8)
}.padding(.trailing, 30)
}.padding(.top, 100)
Spacer()
}
}.onAppear() {
self.textControl = image.favorited
}.background(
Image(uiImage: UIImage(data: image.image)!)
.resizable()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.scaledToFill()
.clipped()
).ignoresSafeArea()
}
}
Try to add .contentShape(Rectangle()) to your buttons. It should limit tappable area to your button view

How to Align an image to the center of a screen using SwiftUI?

I'm trying to align an image to the center of the screen in Swift/SwiftUI.
GeometryReader { geometry in
HStack {
Spacer()
Color.white
.ignoresSafeArea()
Image("1Pzpe")
.frame(width: geometry.size.width-3, height: geometry.size.width-3, alignment: .center)
Spacer()
}
enter image description here
Instead of being centered, my image goes a little more to the right. Any help would be greatly appreciated :)
Using this modifier will let you center images
extension Image {
func centerCropped() -> some View {
GeometryReader { geo in
self
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
}
}
}
You probably don't need GeometryReader for this case
You are putting a Color view aside the image so it would never be centered
you probably just need a ZStack if you want the image over the color
ZStack{
Color.white
.ignoresSafeArea()
Image(systemName: "iphone")
}
to center a image in the view you only need to put the image there also you don't need GeometryReader to fill the image just use .resizable() .scaledToFit() and .padding(3)
ZStack{
Color.white
.ignoresSafeArea()
Image(systemName: "square.fill")
.resizable()
.scaledToFit()
.padding(3)
.foregroundColor(.red)
}

How to create design covering back button and top bar fully with cross button above it?

I am trying to create below design using swiftUI but not able to find proper approach.
I tried but my image is not covering back button but coming below to back button
Expected design
VStack(spacing: 16) {
VStack {
ZStack(alignment: .topTrailing) {
Image("defaultImage")
.resizable()
.frame( height: 200)
Button("Cancel", action: {
print("cancel")
})
.padding(12)
}
}
}
My Design :(
You might want to do a lot of try in SwiftUI to get a custom design right. gave it a quick try for you and added comments on the code so you can understand most of what i did.
this is the quick result i got (with a random background i got on internet)
import SwiftUI
struct FirstView: View{
var body: some View{
NavigationView {
VStack{
NavigationLink(destination: BackgroundImageNav()){
Text("Go to the view")
.font(.title)
}
}
}
}
}
struct BackgroundImageNav: View {
#Environment(\.presentationMode) var presentationMode
// remplacing the back button action, (only problem is that this disable the swipe back)
var body: some View {
ZStack {
// ZStack help you having your image behind the other views
VStack {
Image("defaultImage")
.resizable()
.frame(height: 180)
.edgesIgnoringSafeArea(.all)
Spacer()
// having a spacer that push the image on the top and ignoring the safe area
}
VStack(alignment:.leading){
HStack{
Button(action:{
presentationMode.wrappedValue.dismiss()
// back button action
}){
Image(systemName:"chevron.left")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30)
.foregroundColor(.white)
}
Spacer()
Rectangle()
.fill(Color.white)
.frame(width: 200, height: 80, alignment: .center)
Spacer()
Button(action:{
// do something
}){
Image(systemName: "xmark")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(.white)
}
}.padding()
Text("Title")
.bold()
.font(.title)
.foregroundColor(.black)
.background(Color.white)
.padding(24)
Spacer()
}
}.navigationBarHidden(true)
// removing the bar to customize the back button
}
}
struct backgroundImage_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}

NavigationLink seems to take only a fiew space

I'm working with NavigatonView and NavigationLink, I've made my view like that :
ScrollView{
VStack{
// MARK: - Survey and tips Navigation
HStack(spacing:9){
NavigationLink(destination:Container.sharedContainer.resolve(SurveyListView.self,argument: $VM.occurrences)!){
iconHome(image: "img-survey", icon: "icon-survey", text: "surveys_upper_case")}
NavigationLink(destination:Container.sharedContainer.resolve(SurveyListView.self,argument: $VM.occurrences)!){
iconHome(image: "img-tip", icon: "icon-tip", text: "tips_upper_case")}
}
.frame(maxWidth: .infinity,maxHeight: .infinity)
}
.padding(.horizontal,37)
.background(Color.red)
}
iconHome is another struct which implements view protocol :
struct iconHome : View{
var image:String
var icon:String
var text:LocalizedStringKey
var body : some View{
GeometryReader{ geometry in
ZStack{
Image(self.image)
.renderingMode(.original)
.resizable()
.frame(width: geometry.size.width ,height:geometry.size.width)
.aspectRatio(contentMode: .fit)
.cornerRadius(20)
VStack{
Image(self.icon)
.renderingMode(.original)
.resizable()
.frame(width: geometry.size.width / 5,height:geometry.size.width / 5 )
.aspectRatio(contentMode: .fit)
Text(self.text)
.foregroundColor(.white)
.modifier(OpenSansBoldModifier(fontSize: 12))
}
}
.shadow(radius: 5, x: 5, y: 5)
}
}
}
I've got a strange result :
I don't why but my HStack which contains my two navigation link have only red rectangle high ... so it's hard to click on him ... any idea about why the HStack doesn't have the good high ?
It seems there is missing a Fill mode for the geometry
GeometryReader{ geometry in
....
}.aspectRatio(contentMode: .fill)

Clip image to square in SwiftUI

I am trying to put multiple cells next to each other where each cell consists of an image and a text below. The cell itself should be a square and the image should be scaled to fill the remaining space (cutting a part of the image).
First I tried just making the image square and the text below.
Now my problem is, that I don't know how to properly achieve that in SwiftUI. I can get it to work, when using this code:
VStack {
Image(uiImage: recipe.image!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 200, height: 200, alignment: .center)
.clipped()
Text(recipe.name)
}
The problem is, that I have to specify a fixed frame size. What I want is a way to make a cell, that keeps an aspect ratio of 1:1 and is resizable, so I can fit a dynamic amount of them on a screen next to each other.
I also tried using
VStack {
Image(uiImage: recipe.image!)
.resizable()
.aspectRatio(1.0, contentMode: .fit)
.clipped()
Text(recipe.name)
}
which gives me square images, that scale dynamically. But the problem is, that the image now gets stretched to fill the square and not scaled to fill it.
My next idea was to clip it to a square shape. For that I first tried to clip it into a circle shape (because apparently there is not square shape):
VStack {
Image(uiImage: recipe.image!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
Text(recipe.name)
}
But for some odd reason, it didn't really clip the image but instead kept the remaining space...
So am I not seeing something or is the only option to clip an image square the frame modifier?
EDIT
To clarify: I don't care about the text as much, as about the whole cell (or if it's simpler the image) being square, without having to specify its size via .frame and without the non-square original image being stretched to make it fit.
So the perfect solution would be that the VStack is square but getting a square image would also be okay. It should look like Image 1, but without having to use the .frame modifier.
A ZStack will help solve this by allowing us to layer views without one effecting the layout of the other.
For the text:
.frame(minWidth: 0, maxWidth: .infinity) to expand the text horizontally to its parent's size
.frame(minHeight: 0, maxHeight: .infinity) is useful in other situations
As for the image:
.aspectRatio(contentMode: .fill) to make the image maintain its aspect ratio rather than squashing to the size of its frame.
.layoutPriority(-1) to de-prioritize laying out the image to prevent it from expanding its parent (the ZStack within the ForEach in our case).
The value for layoutPriority just needs to be lower than the parent views which will be set to 0 by default. We have to do this because SwiftUI will layout a child before its parent, and the parent has to deal with the child size unless we manually prioritize differently.
The .clipped() modifier uses the bounding frame to mask the view so you'll need to set it to clip any images that aren't already 1:1 aspect ratio.
var body: some View {
HStack {
ForEach(0..<3, id: \.self) { index in
ZStack {
Image(systemName: "doc.plaintext")
.resizable()
.aspectRatio(contentMode: .fill)
.layoutPriority(-1)
VStack {
Spacer()
Text("yes")
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.white)
}
}
.clipped()
.aspectRatio(1, contentMode: .fit)
.border(Color.red)
}
}
}
Edit: While geometry readers are super useful I think they should be avoided whenever possible. It's cleaner to let SwiftUI do the work. This is my initial solution with a Geometry Reader that works just as well.
HStack {
ForEach(0..<3, id: \.self) { index in
ZStack {
GeometryReader { proxy in
Image(systemName: "pencil")
.resizable()
.scaledToFill()
.frame(width: proxy.size.width)
VStack {
Spacer()
Text("yes")
.frame(width: proxy.size.width)
.background(Color.white)
}
}
}
.clipped()
.aspectRatio(1, contentMode: .fit)
.border(Color.red)
}
}
Here's another solution I found on Reddit and improved a bit:
Color.clear
.aspectRatio(1, contentMode: .fit)
.overlay(
Image(imageName)
.resizable()
.scaledToFill()
)
.clipShape(Rectangle())
It is similar to Chads answer but differs in the way you put image relatively to the clear color (background vs overlay)
Bonus: to let it have circular shape just use .clipShape(Circle()) as the last modifier. Everything else stays unchanged
This is the answer that worked for me:
VStack {
Color.clear
.aspectRatio(1, contentMode: .fit)
.background(Image(uiImage: recipe.image!)
.resizable()
.aspectRatio(contentMode: .fill))
.clipped()
Text(recipe.name)
}
I use clear color, then set the aspect ratio using fit. That makes the container square. Then I added a background of the image set it to fill. To top it off, I add clipped so the background doesn't spill over the edges of the square.
Answer based on the one by #ramzesenok but wrapped into a view modifier
Modifier:
struct FitToAspectRatio: ViewModifier {
let aspectRatio: Double
let contentMode: SwiftUI.ContentMode
func body(content: Content) -> some View {
Color.clear
.aspectRatio(aspectRatio, contentMode: .fit)
.overlay(
content.aspectRatio(nil, contentMode: contentMode)
)
.clipShape(Rectangle())
}
}
You can optionally also add an extension function for easy access
extension Image {
func fitToAspect(_ aspectRatio: Double, contentMode: SwiftUI.ContentMode) -> some View {
self.resizable()
.scaledToFill()
.modifier(FitToAspectRatio(aspectRatio: aspectRatio, contentMode: contentMode))
}
}
and then simply
Image(...).fitToAspect(1, contentMode: .fill)
It works for me, but I don't know why cornerRadius is necessary...
import SwiftUI
struct ClippedImage: View {
let imageName: String
let width: CGFloat
let height: CGFloat
init(_ imageName: String, width: CGFloat, height: CGFloat) {
self.imageName = imageName
self.width = width
self.height = height
}
var body: some View {
ZStack {
Image(imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: width, height: height)
}
.cornerRadius(0) // Necessary for working
.frame(width: width, height: height)
}
}
struct ClippedImage_Previews: PreviewProvider {
static var previews: some View {
ClippedImage("dishLarge1", width: 100, height: 100)
}
}
GeometryReader for frame
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(spacing: 1) {
ForEach(recipes) { recipe in
GalleryView(width: geometry.size.width, recipe: recipe)
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
Overlapping gestures
If you have gesture actions and images close each other, add .contentShape() modifier to assign the tappable area.
Width and height get from GeometryReader of the previous View.
struct GalleryView: View {
var width: CGFloat
var recipe: Recipe
private enum C {
static let goldenRatio = CGFloat(0.67)
}
var body: some View {
VStack(spacing: 1) {
Image(uiImage: recipe.image)
.resizable()
.scaledToFill()
.frame(width: width, height: height, alignment: .center)
.clipped()
.contentShape(Rectangle())
Text(recipe.name)
}
.frame(height: width * C.goldenRatio + 1)
.frame(width: width * C.goldenRatio / 2)
}
}

Resources