2

I want both the buttons to have equal height similar to Equal Height constraint in UIKit.

  • Don't want to specify the frame, let SwiftUI handle it but the elements in HStack should be of the same height.
  • Buttons should have equal width and height and adapt to longer text and increases their frame size
  • Both the buttons should display their complete text (Font to scale / Fit shouldn't be used)

Sample Code


struct SampleView: View {
    var body: some View {
        
        GeometryReader { gr in
            VStack {
                ScrollView {
                    VStack {
                        // Fills whatever space is left
                        Rectangle()
                            .foregroundColor(.clear)

                        Image(systemName: "applelogo")
                            .resizable()
                            .frame(width: gr.size.width * 0.5, height: gr.size.height * 0.3, alignment: .center)
                            //.border(Color.blue)
                            .padding(.bottom, gr.size.height * 0.06)


                        Text("SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME")
                            .fontWeight(.regular)
                            .foregroundColor(.green)
                            .multilineTextAlignment(.center)
                            .padding(.horizontal, 40)
                            .layoutPriority(1)


                        // Fills 15 %
                        Rectangle()
                            .frame(height: gr.size.height * 0.12)
                            .foregroundColor(.clear)

                    DynamicallyScalingView()
                        .padding(.horizontal, 20)
                        .padding(.bottom, 20)

                    }

                    // Makes the content stretch to fill the whole scroll view, but won't be limited (it can grow beyond if needed)
                    .frame(minHeight: gr.size.height)
                }
            }
        }
    }
}

struct DynamicallyScalingView: View {
    @State private var labelHeight = CGFloat.zero     // << here !!

    var body: some View {
        HStack {
            Button(action: {
            }, label: {
                Text("Button 1")
            })
            .foregroundColor(Color.white)
            .padding(.vertical)
            .frame(minWidth: 0, maxWidth: .infinity)
            .frame(minHeight: labelHeight)
            .background(Color.blue)
            .cornerRadius(8)

            Button(action: {
            }, label: {
                Text("Larger Button 2 Text Text2")
            })
            .foregroundColor(Color.white)
            .padding(.vertical)
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(Color.blue)
            .cornerRadius(8)
            .background(GeometryReader {      // << set right side height
                Color.clear.preference(key: ViewHeightKey.self,
                                       value: $0.frame(in: .local).size.height)
            })
        }
        .onPreferenceChange(ViewHeightKey.self) { // << read right side height
            self.labelHeight = $0        // << here !!
        }
        .padding(.horizontal)
    }
}

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

struct SampleView_Previews: PreviewProvider {
    static var previews: some View {
        SampleView().previewDevice("iPhone SE (2nd generation)")
    }
}

Pablo Dev
  • 317
  • 5
  • 16
  • In SwiftUI, parents view cant "force" child view a size like in UIKit. – YodagamaHeshan Jan 07 '21 at 05:46
  • Does this answer your question https://stackoverflow.com/a/62451599/12299030? – Asperi Jan 07 '21 at 06:41
  • What should happen when there is a long text on the 2nd button, should it get clipped? – user1046037 Jan 07 '21 at 06:53
  • @Asperi, Thank you for pointing me in the right direction. Yes, it does work with the way you have pointed out using the preference key. Unfortunately, my view has more elements, let me update the question. – Pablo Dev Jan 07 '21 at 15:46

1 Answers1

3

You can set the max value in the ViewHeightKey preference key:

struct ViewHeightKey: PreferenceKey {
    static var defaultValue: CGFloat { 0 }
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = max(value, nextValue()) // set the `max` value (from both buttons)
    }
}

and then read view height from both buttons and force vertical fixedSize:

struct DynamicallyScalingView: View {
    @State private var labelHeight = CGFloat.zero

    var body: some View {
        HStack {
            Button(action: {}, label: {
                Text("SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME")
            })
                .foregroundColor(Color.white)
                .padding(.vertical)
                .frame(minWidth: 0, maxWidth: .infinity)
                .frame(minHeight: labelHeight) // min height for both buttons
                .background(Color.blue)
                .cornerRadius(8)
                .fixedSize(horizontal: false, vertical: true) // expand vertically
                .background(GeometryReader { // apply to both buttons
                    Color.clear
                        .preference(
                            key: ViewHeightKey.self,
                            value: $0.frame(in: .local).size.height
                        )
                })

            Button(action: {}, label: {
                Text("jahlsd")
            })
                .foregroundColor(Color.white)
                .padding(.vertical)
                .frame(minWidth: 0, maxWidth: .infinity)
                .frame(minHeight: labelHeight)
                .background(Color.blue)
                .cornerRadius(8)
                .fixedSize(horizontal: false, vertical: true)
                .background(GeometryReader {
                    Color.clear
                        .preference(
                            key: ViewHeightKey.self,
                            value: $0.frame(in: .local).size.height
                        )
                })
        }
        .onPreferenceChange(ViewHeightKey.self) {
            self.labelHeight = $0
        }
        .padding(.horizontal)
    }
}

Note: as the buttons are similar now, the next step would be to extract them as another component to avoid duplication.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • Buttons should have equal width and height. Using `fixedSize()` make buttons frame clip to intrinsic size. – Pablo Dev Jan 08 '21 at 03:30
  • @PabloDev You said: *"Button adapts to longer text and increases its frame size"* and *"Both the buttons should display their complete text"*. If the text is longer than half of screen width one of these constraints is invalid. – pawello2222 Jan 08 '21 at 10:06
  • Maybe I was not clear, updated the post description. Thank you for pointing it out. – Pablo Dev Jan 08 '21 at 14:57
  • @PabloDev Thanks but I still don't fully get it. What if one button has a text of 2/3 of screen width? How the layout should look like then? And how the layout should be different from what you have now (both buttons are equal currently)? – pawello2222 Jan 08 '21 at 15:03
  • Buttons will increase in height to accommodate the text. Currently, it behaves so but the frame (height) of both the buttons are not equal. Please do let me know if it's not clear. Ex: we can use `Environment overrides` in Xcode to increase the adaptability font size, both buttons should have equal width and height irrespective of text content. As stated in the earlier requirement, font to scale / fixed Size/font to fill should not be used. Font size should adapt to the dynamic font size chosen on the device – Pablo Dev Jan 08 '21 at 15:11
  • In other words, I am looking for an equivalent behaviour as in UIKit...where we specify Equal Width/Equal Height constraint between elements. – Pablo Dev Jan 08 '21 at 15:15
  • @ pawello2222, Perfect work's like a charm. Thank you so much. I think now I understood how `PreferenceKey` works and how to use them. – Pablo Dev Jan 08 '21 at 18:15
  • @pawello2222 I have a similar situation, where sample view is in the navigation view. height preference key doesn't seem to work. You should be able to test it out by placing the above sample view in the navigation view. any thoughts or ideas?? – Vignan Sankati Jan 09 '21 at 23:27
  • @VignanS hard to say without seeing your code. I suggest you create a new post for this. – pawello2222 Jan 10 '21 at 16:09
  • @pawello2222, I was able to re-create the issue mentioned above. https://stackoverflow.com/q/65674752/13092726 Can you take a look? – Pablo Dev Jan 11 '21 at 21:29