2

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

spoax
  • 471
  • 9
  • 29

2 Answers2

2

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)
    }
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Hi @Asperi, this doesnt create the full width view that I am trying to create. Please, see my first image for the layout I am trying to replicate. – spoax Dec 08 '20 at 19:13
  • Of course, because your DetailView did not cover full screen (title bar + safe area). See updated. – Asperi Dec 08 '20 at 19:23
  • I have just tried this in my real project using images instead of rectangles and it doesn't do the same thing. In my question I stated that I wanted the images bounding box to change so they always meet in the middle of the screen and then inside the bounding boxes of each image to maintain the the correct aspect ratio. I guess this would give a zooming effect when the navigation side bar is displayed. – spoax Dec 09 '20 at 09:39
1

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 (16x9). Assuming the images are pretty much standard sizes, or use whatever you like (4x6, 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.

Dabble
  • 287
  • 1
  • 4
  • Thank you so much, this is perfect and well explained! I was struggling to find anything online that explained it well. Thanks again – spoax Dec 13 '20 at 19:05