5

I have been trying to achieve to match timeline sidebar height as per the content below header.

But side bar is taking full height, is there any way we can restrict it's height as of middle content. it's in HStackView as below.

            //header
            HStack {
                Circle()
                    .fill(Color.blue)
                    .frame(width: 15, height: 15)
                    .overlay(Circle().inset(by: 2).fill(Color.white))
                
                Text("Headline").font(.headline)
            }.padding(0)

            //content
            HStack {
                VStack {
//                    Text("l").padding(.leading,5)
//                    Text("l").padding(.leading,5)
                    Rectangle().frame(width: 20)
                }
                
                Text("Time Line Content Time Line Content Time Line Content Time Line Content fdfgfuysdgfuydsgfgds fgfyusdfyfdsdfsdfgyusdg fydsufidsfy uyfudsfuydsufysdfsdf dfusdtfoisdtftdsoftsdftsydtfsdtfodstfdstf fgdsygfdsgfuygu").font(.caption)
            }
            
            //footer
            HStack {
                Circle()
                    .fill(Color.orange)
                    .frame(width: 15, height: 15)
                    .overlay(Circle().inset(by: 2).fill(Color.white))
                
                Text("Footer").font(.subheadline)
            }.padding(0)

Thanks.

TimelineContentView

khanHussain
  • 97
  • 1
  • 8

3 Answers3

5

From what I understood of your question is that:

  1. You have an HStack in which the leftmost view is a Rectangle and the rightmost view is a Text.
  2. You want the Rectangle to be the same height as the Text.

The problem is that the height of the HStack is based on the tallest child view which happens to be the Rectangle but a Rectangle view does not have any intrinsic size like Text and will occupy all the space the parent provides, or if you manually apply a frame.
You set a width of 20 but leave height and so it takes the entire height it can get.

This indicates that we need to set the height of the Rectangle to be same as the dynamic Text but the problem is that we don't know the height upfront.

To solve this:

  1. First we need to know the height of the dynamic Text.
  2. The height is in a child view so we need it to notify the parent it's height value.
  3. The parent view should update the Rectangle when it gets to know the Text height
    • A simple @State variable will suffice now

Solution:

struct ContentLengthPreference: PreferenceKey {
   static var defaultValue: CGFloat { 0 }
   
   static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
      value = nextValue()
   }
}

struct ContentView: View {
   @State var textHeight: CGFloat = 0 // <-- this
   
   var body: some View {
      HStack {
        Rectangle()
           .frame(width: 20, height: textHeight) // <-- this
        
        Text(String(repeating: "lorem ipsum ", count: 25))
           .overlay(
              GeometryReader { proxy in
                Color
                   .clear
                   .preference(key: ContentLengthPreference.self,
                               value: proxy.size.height) // <-- this
              }
           )
      }
      .onPreferenceChange(ContentLengthPreference.self) { value in // <-- this
        DispatchQueue.main.async {
           self.textHeight = value
        }
      }
   }
}
  1. Create ContentLengthPreference as our PreferenceKey implementation
  2. on Text; Apply overlay containing GeometryReader
  3. overlay will have same height as Text
  4. in GeometryReader, Color.clear is just a filler invisible view
  5. anchorPreference modifier allows us to access and store height
  6. onPreferenceChange modifier on parent HStack can catch the value passed by child view
  7. parent saves the height to a state property textHeight
  8. textHeight can be applied on Rectangle and will update the view when this value updates

Credits: https://www.wooji-juice.com/blog/stupid-swiftui-tricks-equal-sizes.html


Output (including your header + footer views):

Output


EDIT:

If you have multiple of these in a List then you don't need to do anything. Each row will size automatically upto the Text height.
It's free!!!

struct ContentView: View {
   var body: some View {
      List(0..<20) { _ in
        ArticleView()
      }
   }
}

struct ArticleView: View {
   var body: some View {
      VStack(alignment: .leading) {
        HStack {
           Circle()
              .fill(Color.blue)
              .frame(width: 15, height: 15)
              .overlay(Circle().inset(by: 2).fill(Color.white))
           
           Text("Headline").font(.headline)
        }
        
        HStack {
           Rectangle().frame(width: 20)
           
           Text(String(repeating: "lorem ipsum ", count: (5...50).randomElement()!))
        }
        
        HStack {
           Circle()
              .fill(Color.orange)
              .frame(width: 15, height: 15)
              .overlay(Circle().inset(by: 2).fill(Color.white))
           
           Text("Footer").font(.subheadline)
        }
      }
   }
}
staticVoidMan
  • 19,275
  • 6
  • 69
  • 98
  • Yes, exactly what I’m looking for. Thanks for detailed explanation. – khanHussain Mar 02 '21 at 14:12
  • While using in ScrollView with multiple cells, we get fluctuations on screen and in console "Bound preference ContentLengthPreference tried to update multiple times per frame." – khanHussain Mar 02 '21 at 14:53
  • I don't know why this is too much ???? this will rendering your view many times. – Raja Kishan Mar 02 '21 at 15:49
  • @khanHussain This seems to cause infinite rendering in a `ScrollView`. However, if you use `List` then you don't need to do anything! You will get your expected UI for free. Check updated answer. Just plain SwiftUI, nothing fancy. If that is still not good enough then please update your question with full details of `ScrollView`. – staticVoidMan Mar 02 '21 at 18:01
  • @RajaKishan Yes, this is problematic in a `ScrollView` as frame updation starts to go into a loop. I didnt account for this but hopefully the alternate solution might help OP. – staticVoidMan Mar 02 '21 at 18:16
  • 1
    Thanks @staticVoidMan. Using List, its working without extra efforts – khanHussain Mar 09 '21 at 12:59
2

A neat option is to combine an overlay and the GeometryReader:

HStack {
          Text("some long text........")
               .font(.body)
               .padding(.horizontal, 20)
        }
        .overlay(
            GeometryReader { proxy in
                Rectangle()
                    .frame(width: 20, height: proxy.size.height)
            }
        )
lewis
  • 2,936
  • 2
  • 37
  • 72
Pavan
  • 49
  • 1
  • 7
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Sep 30 '21 at 04:44
1

You can add max-height like this

HStack {
    VStack {
        //                    Text("l").padding(.leading,5)
        //                    Text("l").padding(.leading,5)
        Rectangle().frame(width: 20)
    }
    
    Text("Time Line Content Time Line Content Time Line Content Time Line Content fdfgfuysdgfuydsgfgds fgfyusdfyfdsdfsdfgyusdg fydsufidsfy uyfudsfuydsufysdfsdf dfusdtfoisdtftdsoftsdftsydtfsdtfodstfdstf fgdsygfdsgfuygu").font(.caption)
}.frame(minHeight: 0, maxHeight: .infinity) //<---here

Or Set up your cell view like this

struct ContentCellView: View {
    var isLast: Bool = false
    var body: some View {
        HStack(alignment: .top, spacing: 0) {
            VStack(alignment: .leading) {
                Image(systemName: "message.circle").frame(width: 30)
                if !isLast {
                    Rectangle().fill(Color.black).frame(width: 1).padding(.leading, 15.5)
                }
            }
            Text("Time Line Content Time Line Content Time Line Content Time Line Content fdfgfuysdgfuydsgfgds fgfyusdfyfdsdfsdfgyusdg fydsufidsfy uyfudsfuydsufysdfsdf dfusdtfoisdtftdsoftsdftsydtfsdtfodstfdstf fgdsygfdsgfuygu")
        }.frame(minHeight: 0, maxHeight: .infinity)
    }
}

struct ContentViewList: View {
    var body: some View {
        ScrollView{
            VStack(spacing: 0){
                ForEach((1...10).reversed(), id: \.self) {
                    ContentCellView(isLast: $0 == 1) // isLast for not showing last line in last cell
                }
            }
        }.padding()
    }
}

enter image description here

Raja Kishan
  • 16,767
  • 2
  • 26
  • 52