0

Problem:
I have a View that I needed to place multiple (2) views that contained: 1 Image + 1 Text. I decided to break that up into a ClickableImageAndText structure that I called on twice. This works perfectly if the image is a set size (64x64) but I would like this to work on all size classes. Now, I know that I can do the following:

if horizontalSizeClass == .compact {
    Text("Compact")
} else {
    Text("Regular")
}

but I am asking for both Different Size Classes and Same Size Classes such as the iPhone X and iPhone 13 which are the same.

Question:
How do I alter the image for dynamic phone sizes (iPhone X, 13, 13 pro, etc) so it looks appropriate for all measurements?

Code:

import SwiftUI

struct ClickableImageAndText: View {
    let image: String
    let text: String
    let tapAction: (() -> Void)
    
    var body: some View {
        VStack {
            Image(image)
                .resizable()
                .scaledToFit()
                .frame(width: 64, height: 64)
            Text(text)
                .foregroundColor(.white)
        }
        .contentShape(Rectangle())
        .onTapGesture {
            tapAction()
        }
    }
}

struct InitialView: View {
    
    var topView: some View {
        Image("Empty_App_Icon")
            .resizable()
            .scaledToFit()
    }
    
    var bottomView: some View {
        VStack {
            ClickableImageAndText(
                image: "Card_Icon",
                text: "View Your Memories") {
                    print("Tapped on View Memories")
                }
                .padding(.bottom)
            ClickableImageAndText(
                image: "Camera",
                text: "Add Memories") {
                    print("Tapped on Add Memories")
                }
                .padding(.top)
        }
    }
    
    var body: some View {
        ZStack {
            GradientView()
            VStack {
                Spacer()
                topView
                Spacer()
                bottomView
                Spacer()
            }
        }
    }
}

struct InitialView_Previews: PreviewProvider {
    static var previews: some View {
        InitialView()
    }
}

Image Note:
My background includes a GradientView that I have since removed (thanks @lorem ipsum). If you so desire, here is the GradientView code but it is unnecessary for the problem above.

GradientView.swift

import SwiftUI

struct GradientView: View {
    let firstColor = Color(uiColor: UIColor(red: 127/255, green: 71/255, blue: 221/255, alpha: 1))
    let secondColor = Color(uiColor: UIColor(red: 251/255, green: 174/255, blue: 23/255, alpha: 1))
    let startPoint = UnitPoint(x: 0, y: 0)
    let endPoint = UnitPoint(x: 0.5, y: 1)
    
    
    var body: some View {
        LinearGradient(gradient:
            Gradient(
                colors: [firstColor, secondColor]),
                startPoint: startPoint,
                endPoint: endPoint)
            .ignoresSafeArea()
    }
}

struct GradientView_Previews: PreviewProvider {
    static var previews: some View {
        GradientView()
    }
}

Set 64x64 size

Effort 1:
Added a GeometryReader to my ClickableImageAndText structure and the view is automatically changed incorrectly.

struct ClickableImageAndText: View {
    let image: String
    let text: String
    let tapAction: (() -> Void)
    
    var body: some View {
        GeometryReader { reader in
            VStack {
                Image(image)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 64, height: 64)
                Text(text)
                    .foregroundColor(.white)
            }
            .contentShape(Rectangle())
            .onTapGesture {
                tapAction()
            }
        }
    }
}

Effort 1

Effort 2:
Added a GeometryReader as directed by @loremipsum's [deleted] answer and the content is still being pushed; specifically, the topView is being push to the top and the bottomView is taking the entire space with the addition of the GeometryReader.

struct ClickableImageAndText: View {
    let image: String
    let text: String
    let tapAction: (() -> Void)
    
    var body: some View {
        GeometryReader{ geo in
            VStack {
                Image(image)
                    .resizable()
                    .scaledToFit()
                //You can do this and set strict size constraints
                //.frame(minWidth: 64, maxWidth: 128, minHeight: 64,  maxHeight: 128, alignment: .center)
                //Or this to set it to be percentage of the size of the screen
                    .frame(width: geo.size.width * 0.2, alignment: .center)
                Text(text)
                
            }.foregroundColor(.white)
            //Everything moves to the left because the `View` expecting a size vs stretching.
            //If yo want the entire width just set the View with on the outer most View
                .frame(width: geo.size.width, alignment: .center)
        }
        .contentShape(Rectangle())
        .onTapGesture {
            tapAction()
        }
    }
}

Effort 2

10
  • Use Geometry reader and percentages based size Commented Dec 27, 2021 at 9:49
  • GeometryReader removes the Spacers which I need to maintain my current structure. Commented Dec 27, 2021 at 9:54
  • @loremipsum added images/efforts for this as well Commented Dec 27, 2021 at 9:59
  • @impression7vx For "How do I alter the image for dynamic phone sizes ... so it looks appropriate for all measurements?": What do you mean? How will you adjust for different sizes, by size class or by relative width of screen / parent view? Commented Dec 30, 2021 at 19:27
  • "but I am asking for both Different Size Classes and Same Size Classes such as the iPhone X and iPhone 13 which are the same" - Both! It seems size classes are by .compact and .regular comparisons but I was also wanting relativeness. Commented Dec 30, 2021 at 19:30

1 Answer 1

1
+50

The possible solution is to use screen bounds (which will be different for different phones) as reference value to calculate per-cent-based dynamic size for image. And to track device orientation changes we wrap our calculations into GeometryReader.

Note: I don't have your images, so added white borders for demo purpose

demo

struct ClickableImageAndText: View {
    let image: String
    let text: String
    let tapAction: (() -> Void)

    @State private var size = CGFloat(32) // some minimal initial value (not 0)

    var body: some View {
        VStack {
            Image(image)
                .resizable()
                .scaledToFit()
                // .border(Color.white)            // << for demo !!
                .background(GeometryReader { _ in
                    // GeometryReader is needed to track orientation changes
                    let sizeX = UIScreen.main.bounds.width
                    let sizeY = UIScreen.main.bounds.height

                    // Screen bounds is needed for reference dimentions, and use
                    // it to calculate needed size as per-cent to be dynamic
                    let width = min(sizeX, sizeY)
                    Color.clear                             // % (whichever you want)
                        .preference(key: ViewWidthKey.self, value: width * 0.2) 
                })
                .onPreferenceChange(ViewWidthKey.self) {
                    self.size = max($0, size)
                }
                .frame(width: size, height: size)

            Text(text)
                .foregroundColor(.white)
        }
        .contentShape(Rectangle())
        .onTapGesture {
            tapAction()
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.