iosswiftuicardview

Image adjustment not fixed to position when Text comes in two lines in TileView SwiftUI


I have created a CardView/Tile view to display as a two column. I have calculated the cardView width and height according to screen size of different devices.

The problem I am facing is adjustment of Image. I also have a text just below to Image. Overall Image is fixed to its position but whenever text comes in two lines limitline(2) the image little move to upward (top) position. I set aspect ratio but its not working properly.

Please help me out what I am doing wrong exactly. Can I set image according to screen size as I did for Card/Tile view? It picture you can see first tile image is moved up side.

Code:

import SwiftUI

struct TileView: View {
    @StateObject var viewModel: CartViewModel
    
    // MARK: - Constants
    private struct ViewConstants {
        static let screenWidth = Constants.DeviceConfig.screenWidth
        static let cardRatio = 133.0/148.0
        static let cardWidth = screenWidth / 2 - 24
        static let cardHeight = cardWidth/cardRatio
        
        static let imageWidth: CGFloat = 106
        static let imageHeight: CGFloat = 106
    }
    
    var body: some View {
            VStack(spacing: 8) {
                Image(viewModel.image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(height: ViewConstants.imageHeight)
                
                Text(viewModel.title)
                    .lineLimit(2)
                    .multilineTextAlignment(.center)
                    .padding(.horizontal, 16)
            }
            .frame(width: ViewConstants.cardWidth, height: ViewConstants.cardHeight)
            .background(Color.gray)
            .cornerRadius(10)
    }
}

Image: TileView


Solution

  • This is happening because the VStack with the image and the text has more height when the text goes over two lines.

    You are setting a frame on the VStack like this:

    .frame(width: ViewConstants.cardWidth, height: ViewConstants.cardHeight)
    

    The alignment here is the default of .center, so the middle of the VStack is vertically centered. This means, when there are two lines of text, the image is going to be a little higher than when there is only one line.

    Here are some ways to fix:

    1. Apply a minimum height to the text

    By setting a minimum height on the text, you can make sure it always occupies enough space to extend over two lines:

    Text(viewModel.title)
        .lineLimit(2)
        .multilineTextAlignment(.center)
        .padding(.horizontal, 16)
        .frame(minHeight: 40, alignment: .top) // <- HERE
    

    2. Use hidden text to establish the footprint

    If you don't like using a fixed minimum height, then you can determine the minimum height needed by using some hidden text. Then you can show your actual (visible) text in an overlay:

    Text(".\n.")
        .hidden()
        .frame(maxWidth: .infinity)
        .overlay(alignment: .top) {
            Text(viewModel.title)
                .lineLimit(2)
                .multilineTextAlignment(.center)
                .padding(.horizontal, 16)
        }
    

    The hidden text consists of a dot, a newline character and another dot. This is sufficient for finding the height needed for two lines of text.

    3. Align to the top of the frame

    If the height of the tile is fixed (which it appears to be) then you could decide what space you want between the top of the tile and the image and apply this as top padding to the image. Then you can use .top alignment when you set the frame on the VStack:

    VStack(spacing: 8) {
        Image(...)
            // other modifiers as before
            .padding(.top, 20) // <- ADDED
    
        Text(...)
            // other modifiers as before
    }
    .frame(width: ViewConstants.cardWidth, height: ViewConstants.cardHeight, alignment: .top) // alignment added
    

    Screenshot


    EDIT So you said in a comment that the spacing between the text and the bottom of the tile should be 20. This can be defined using padding. Here is how you can use a combination of techniques 1 + 3 to use all the space available with spaces of 20 above and below:

    Image(viewModel.image)
        .resizable()
        .aspectRatio(contentMode: .fit)
        .padding(.horizontal) // if needed
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .padding(.top, 20)
    Text(viewModel.title)
        .lineLimit(2)
        .multilineTextAlignment(.center)
        .frame(minHeight: 40, alignment: .top)
        .padding(.horizontal, 16)
        .padding(.bottom, 20)