skip to Main Content

I have 2 HStack‘s using geometry reader to split them evenly into 2 sections that are embedded into a VStack, I am trying to create a layout similar to the first below image (landscape mode on iPad).

However, I am struggling to get the HStack‘s to line up like a grid meeting in the middle. I also have a NavigationView sidebar and which can be presented alongside the HStacks so ideally the 2 images would change widths but keep their height without squashing or stretching the images. I have tried to do this using clipped().

The second image below is what I am getting when I run my code. I have replaced the images in this example to SFSymbol to make it easier to debug.

desired layout

current layout

This is the NavigationView sidebar that is being called in my ContentView:

struct SideBar: View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink(destination: DetailView()) {
                    Label("Products", systemImage: "printer")
                }
                Label("Comparison", systemImage: "simcard.2")
                Label("Search", systemImage: "magnifyingglass")
            }
            .listStyle(SidebarListStyle())
            .navigationTitle("Navigation")
            
            DetailView()
        }
    }
}

This is the main view that holds the content:

struct DetailView: View {
    
    let title = "This is a title"
    let paragraph = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
    let image = "dot.squareshape.fill"
    let intPadding: CGFloat = 10
    let extPadding: CGFloat = 40
    
    var body: some View {
        VStack(spacing: 0){
            GeometryReader { geometry in
                HStack(alignment: .top, spacing: 0){
                    Image(systemName:image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: geometry.size.width / 2)
                        .clipped()
                    VStack(alignment: .leading) {
                        Text(title)
                            .font(.custom("Avenir-Heavy", size: 30))
                            .multilineTextAlignment(.leading)
                            .padding(.leading, intPadding)
                        Text(paragraph)
                            .font(.custom("Avenir", size: 16))
                            .multilineTextAlignment(.leading)
                            .lineSpacing(10)
                            .padding(.leading, intPadding)
                            .padding(.trailing, extPadding)
                    }
                    .frame(width: geometry.size.width / 2)
                }

            }
            GeometryReader { geometry in
                HStack(alignment: .top, spacing: 0){
                    Text(paragraph)
                        .font(.custom("Avenir", size: 16))
                        .multilineTextAlignment(.leading)
                        .lineSpacing(10)
                        .frame(width: geometry.size.width / 2)
                        .padding(.top, intPadding)
                        .padding(.trailing, intPadding)
                        .padding(.leading, extPadding)
                    Image(systemName:image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: geometry.size.width / 2)
                        .offset(x: -intPadding)
                        .clipped()
                }
            }
        }
    }
}

EDIT:

The black squares are representing where images are going to go, I am not putting black squares. So the idea of the squashing and stretching mentioned above is supposed to look like the below image, so it doesn’t actually stretch or squash the image just the bounding box:

enter image description here

2

Answers


  1. Here is a demo of possible approach – use Color.clear, as it fills everything available equally, with content in overlays.

    Prepared with Xcode 12.1 / iOS 14.1

    demo

    var body: some View {
        VStack(spacing: 0) {
            Color.clear.overlay(
                HStack(spacing: 0) {
                        Color.clear.overlay(
                    Image(systemName:image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        )
                        Color.clear.overlay(
                    VStack(alignment: .leading) {
                        Text(title)
                            .font(.custom("Avenir-Heavy", size: 30))
                            .multilineTextAlignment(.leading)
                            .padding(.leading, intPadding)
                        Text(paragraph)
                            .font(.custom("Avenir", size: 16))
                            .multilineTextAlignment(.leading)
                            .lineSpacing(10)
                            .padding(.leading, intPadding)
                            .padding(.trailing, extPadding)
                    }
                        , alignment: .top)
                    },
            alignment: .top)
            Color.clear.overlay(
                HStack(spacing: 0) {
                        Color.clear.overlay(
                    Text(paragraph)
                        .font(.custom("Avenir", size: 16))
                        .multilineTextAlignment(.leading)
                        .lineSpacing(10)
                        .padding(.top, intPadding)
                        .padding(.trailing, intPadding)
                        .padding(.leading, extPadding)
                        , alignment: .top)
                        Color.clear.overlay(
                    Image(systemName:image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .clipped()
                        , alignment: .top)
                }
            )
        }
    }
    

    Update: full-screen variant

    demo2

            VStack(spacing: 0) {
                Color.clear.overlay(
                    HStack(spacing: 0) {
                            Color.black
    //                      .overlay(
    //                    Image(systemName:image)
    //                        .resizable()
    //                        .aspectRatio(contentMode: .fit)
    //                      )
                            Color.clear.overlay(
                        VStack(alignment: .leading) {
                            Text(title)
                                .font(.custom("Avenir-Heavy", size: 30))
                                .multilineTextAlignment(.leading)
                                .padding(.leading, intPadding)
                            Text(paragraph)
                                .font(.custom("Avenir", size: 16))
                                .multilineTextAlignment(.leading)
                                .lineSpacing(10)
                                .padding(.leading, intPadding)
                                .padding(.trailing, extPadding)
                        }
                            , alignment: .top)
                        },
                alignment: .top)
                Color.clear.overlay(
                    HStack(spacing: 0) {
                            Color.clear.overlay(
                        Text(paragraph)
                            .font(.custom("Avenir", size: 16))
                            .multilineTextAlignment(.leading)
                            .lineSpacing(10)
                            .padding(.top, intPadding)
                            .padding(.trailing, intPadding)
                            .padding(.leading, extPadding)
                            , alignment: .top)
                            Color.black
    //                      .overlay(
    //                    Image(systemName:image)
    //                        .resizable()
    //                        .aspectRatio(contentMode: .fit)
    //                        .clipped()
    //                      , alignment: .top)
                    }
                )
            }
            .navigationBarHidden(true)
            .edgesIgnoringSafeArea(.all)
        }
    
    Login or Signup to reply.
  2. The biggest challenge for this layout is height. On each HStack row you have variable multi-line text (which can change size depending on accessibility, fonts, etc). If the text is longer than expected or user has increased it, the layout will not hold.

    To derive the row height, you can use an aspect ratio to set the height of the Image frames and variable Text blocks. This locks the row height for both img/text so the image corners are always touching no matter what the screen width or text length. Longer text will end up getting an ellipse, or you can use a ScrollView and set its height the same as the Image derived aspectHeight.

    The code below cleans up all the padding/offsets causing the horizontal spread issues and uses an image aspect ratio (16×9). Assuming the images are pretty much standard sizes, or use whatever you like (4×6, etc). Note the images weren’t "zooming" correctly using "fit", use aspectRatio.fill to stretch the image out from its center equally.

    If you don’t want to pre-define the image aspect ratio and need the images to have pixel perfect aspect, Swift can pre-load the image files to get the aspect:
    SwiftUI: How to find the height of an image and use it to set the size of a frame

    struct DetailView: View {
    
      let title = "This is a title"
      let paragraph = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
      let image = "dot.squareshape.fill"
      let intPadding: CGFloat = 20
      let extPadding: CGFloat = 20 // reduced extPadding, it seemed high
      
        var body: some View {
        GeometryReader { geometry in
            
            let halfWidth:CGFloat = (geometry.size.width * 0.5) - extPadding
            let aspectHeight:CGFloat = halfWidth * (9/16) // images are 16x9 aspect ratio
    
            // hstack centers layout after extPadding is subracted from desired width
            HStack(alignment: .top) {
                Spacer() // left margin
                VStack(alignment:.center, spacing:0) {
                    // row 1
                    HStack(spacing:0) {
                        Image(systemName:image)
                            .resizable()
                            .aspectRatio(contentMode:.fill) // zoom image keeping correct aspect
                            .frame(width:halfWidth, height:aspectHeight, alignment: .center) // center image to frame
                            .clipped() // clipped crops off the img sides outside the frame
    
                        VStack(alignment:.leading) {
                            Text(title)
                                .font(.custom("Avenir-Heavy", size: 30))
                                .multilineTextAlignment(.leading)
                            Text(paragraph)
                                .font(.custom("Avenir", size: 16))
                                .multilineTextAlignment(.leading)
                                .lineSpacing(10)
                        }
                        .padding(intPadding)
                        .frame(width:halfWidth, height:aspectHeight, alignment: .top) // height stops vertical spread of images due to text length
                    }
                    // row 2
                    HStack(spacing:0) {
                        Text(paragraph)
                            .font(.custom("Avenir", size: 16))
                            .multilineTextAlignment(.leading)
                            .lineSpacing(10)
                            .padding(intPadding)
                            .frame(width:halfWidth, height:aspectHeight, alignment: .top)
    
                        Image(systemName: image)
                            .resizable()
                            .aspectRatio(contentMode:.fill) // zoom image keeping correct aspect
                            .frame(width:halfWidth, height:aspectHeight, alignment: .center) // center image to frame
                            .clipped() // clipped crops off the img sides outside the frame
                    }
                }
                Spacer() // right margin
            }
        } // geo
      } // body
        
    }
    

    Note: LazyVStack has a grid column feature but requires iOS14+ so I stuck with VStack/HStack.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search