SwiftUI List Rows Different Heights for Different System Images - swift

In my SwiftUI app, I have a List inside of a sidebar NavigationView with 2 rows, like this:
List {
NavigationLink(destination: MyView1()) {
Label("Test Row 1", systemImage: "list.bullet")
}
NavigationLink(destination: MyView2()) {
Label("Test Row 2", systemImage: "shippingbox")
.frame(maxWidth: .infinity, alignment: .leading)
}
}
However the second row is taller that the first row, I believe due to the image. If I change the image to this:
NavigationLink(destination: MyView2()) {
Label("Test Row 2", systemImage: "list.number")
.frame(maxWidth: .infinity, alignment: .leading)
}
the rows are now the same height. I don't want to fix the height of the rows, but I was wondering if there was a solution where the rows could be the same height.

If you want to ensure that the rows are even, you can use PreferenceKeys to set the row heights to the height of the tallest row like this:
struct EvenHeightListRow: View {
#State var rowHeight = CGFloat.zero
var body: some View {
List {
NavigationLink(destination: Text("MyView1")) {
Label("Test Row 1)", systemImage: "list.bullet")
// This reads the height of the row
.background(GeometryReader { geometry in
Color.clear.preference(
key: HeightPreferenceKey.self,
value: geometry.size.height
)
})
.frame(height: rowHeight)
}
NavigationLink(destination: Text("MyView2")) {
Label("Test Row 2)", systemImage: "shippingbox")
.background(GeometryReader { geometry in
Color.clear.preference(
key: HeightPreferenceKey.self,
value: geometry.size.height
)
})
.frame(height: rowHeight)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
// this sets backgroundSize to be the max value of the tallest row
.onPreferenceChange(HeightPreferenceKey.self) {
rowHeight = max($0, rowHeight)
}
}
}
// This is the actual preferenceKey that makes it work.
fileprivate struct HeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}

Related

SwiftUI - LazyVStack pinnedViews in another

How to create something like this
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
Section(header: Text("foo") ) {
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
Section (header: Text("bar") ) {
Text("1")
Text("2")
...
}
Section (header: Text("baz") ) {
Text("1")
Text("2")
...
}
}
}
}
I need two fixed header but in this solution headers colide when fixed position
Thanks
You can give the sub headers a background (in white / bg color), and also have to push the first header back using zIndex.
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, pinnedViews: [.sectionHeaders]) {
Section(header:
Text("Overall Header")
.zIndex(-1) // zIndex here
) {
LazyVStack(alignment: .leading, pinnedViews: [.sectionHeaders]) {
Section (header:
Text("Header One")
// give background here
.frame(maxWidth: .infinity, alignment: .leading)
.background(.gray)
) {
ForEach(0..<30) { item in
Text("Item \(item)")
}
}
Section (header:
Text("Header Two")
// give background here
.frame(maxWidth: .infinity, alignment: .leading)
.background(.gray)
) {
ForEach(0..<50) { item in
Text("Item \(item)")
}
}
}
}
}
}
.font(.title2)
.padding()
}
}
Here is some hack :)
It' using the package "SwiftUITrackableScrollView" to be found here:
"https://github.com/maxnatchanon/trackable-scroll-view".
But there are several solutions for a trackable scrollview you can find, or implement one by yourself.
It works fine, the only problem is to get/define the relevant positions of headers. Its easy if you have a predefined/fixed item height.
import SwiftUI
import SwiftUITrackableScrollView
struct MyHeaders: Identifiable {
let id = UUID()
let title: String
let position: CGFloat
var active: Bool = false
}
struct ContentView: View {
#State private var offset = CGFloat.zero // Content offset available to use
#State private var myHeaders = [
MyHeaders(title: "Outer Header One", position: 0),
MyHeaders(title: "Inner Header One", position: 0),
MyHeaders(title: "Inner Header Three", position: 264),
MyHeaders(title: "Outer Header Two", position: 525),
MyHeaders(title: "Inner Header Three", position: 525),
]
var body: some View {
TrackableScrollView(.vertical, showIndicators: false, contentOffset: $offset) {
LazyVStack(alignment: .leading) {
Text("Outer Header One")
Text("Inner Header One")
ForEach(0..<10) { item in
Text("Item \(item)")
}
Text("Inner Header Two")
ForEach(0..<10) { item in
Text("Item \(item)")
}
Text("Outer Header Two")
Text("Inner Header Three")
ForEach(0..<30) { item in
Text("Item \(item)")
}
}
}
.onChange(of: offset) { newValue in
print(offset)
for i in 0..<myHeaders.count {
myHeaders[i].active = myHeaders[i].position < offset
}
}
.overlay(
VStack(alignment: .leading) {
ForEach(myHeaders) { header in
if header.active {
Text(header.title)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.orange)
.background(.background)
, alignment: .topLeading
)
.font(.title2)
.padding()
}
}

Get and set max widths between multiple SwiftUI views

Xcode 12.3 | SwiftUI 2.0 | Swift 5.3
I have multiple HStack whose first element must have the same width, but I cannot know how to achieve this.
HStack {
Text("Label 1:")
Text("Random texts")
}
HStack {
Text("Label 2 Other Size:")
Text("Random texts")
Text("Additional text")
}
HStack {
Text("Label 3 Other Larger Size:")
Text("Random texts")
}
This will display this:
Label 1: Random Texts
Label 2 Other Size: Random Texts Additional Text
Label 3 Other Larger Size: Random Texts
And I want to display this without using VStacks, because each HStack is a List Row:
Label 1: Random Texts
Label 2 Other Size: Random Texts Additional Text
Label 3 Other Larger Size: Random Texts
[__________________________] [_____________...
same size
I tried using a #propertyWrapper that stores the GeometryProxy of each Label's background and calcs the max WrappedValuein order to set the .frame(maxWidth: value) of each Label, but without success, I cannot have working that propertyWrapper (I get a loop and crash).
struct SomeView: View {
#MaxWidth var maxWidth
var body: some View { 
HStack {
Text("Label 1:")
.storeGeo($maxWidth)
.frame(maxWidth: _maxWidth.wrappedValue)
Text("Random texts")
}
HStack {
Text("Label 2 Larger Size:")
.storeGeo($maxWidth)
.frame(maxWidth: _maxWidth.wrappedValue)
// Continue as the example above...
...
...
You can pass a GeometryReader width into any child view by using a standard var (not #State var).
Try this code out, its using GeometryReader and a list of Identifiable items which are passed a "labelWidth" they all render the same width for all labels. Note: I used a VStack in the row so it looked better, didn't follow why a row can't have a VStack in it.
[![struct Item : Identifiable {
var id:UUID = UUID()
var label:String
var val:\[String\]
}
struct ItemRow: View {
var labelWidth:CGFloat = 0
var item:Item
var body: some View {
HStack(spacing:5) {
Text(item.label)
.frame(width:labelWidth, alignment: .leading)
VStack(alignment: .leading) {
ForEach(0 ..< item.val.indices.count) { idx in
Text(item.val\[idx\])
}
}
}
}
}
struct ContentView : View {
#State var items:\[Item\] = \[
Item(label:"Label 1:", val:\["Random texts"\]),
Item(label:"Label 2 Other Size:", val:\["Random texts","Additional text"\]),
Item(label:"Label 3 Other Larger Size:", val:\["Random texts", "Random texts and texts", "Random texts", "Random texts"\])
\]
var body: some View {
GeometryReader { geo in
let labelWidth = geo.size.width * 0.6 // set this however you want to calculate label width
List(items) { item in
ItemRow(labelWidth: labelWidth, item: item)
}
}
}
}][1]][1]
The easiest way would be to set a specific .frame(width:) for the first Text() within the HStack. You can then use .minimumScaleFactor() to resize the text within to avoid the words being cut off. Here's an example to get you started:
struct ContentView: View {
var body: some View {
VStack {
CustomRow(title: "Label 1:", otherContent: ["Random Texts"])
CustomRow(title: "Label 2 Other Size::", otherContent: ["Random Texts", "Additional Text",])
CustomRow(title: "Label 3 Other Larger Size:", otherContent: ["Random Texts"])
}
}
}
struct CustomRow: View {
let title: String
let otherContent: [String]
var body: some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
.lineLimit(1)
.minimumScaleFactor(0.5)
ForEach(otherContent, id: \.self) { item in
Text(item)
.lineLimit(1)
.minimumScaleFactor(0.1)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
Result:
Balanced widths between views
Based on all responses I coded a simpler way to get this working with a modifier.
First, we have to add the necessary extensions with the view modifier and the width getter:
extension View {
func balanceWidth(store width: Binding<CGFloat>, alignment: HorizontalAlignment = .center) -> some View {
modifier(BalancedWidthGetter(width: width, alignment: alignment))
}
#ViewBuilder func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
struct BalancedWidthGetter: ViewModifier {
#Binding var width: CGFloat
var alignment: HorizontalAlignment
func body(content: Content) -> some View {
content
.background(
GeometryReader { geo in
Color.clear.frame(maxWidth: .infinity)
.onAppear {
if geo.size.width > width {
width = geo.size.width
}
}
}
)
.if(width != .zero) { $0.frame(width: width, alignment: .leading) }
}
}
Usage
With this, all the work is done. In order to get equal widths between views all we have to do is mark each view with the balancedWidth modifier and store the shared width value in a #State variable with initial value == .zero:
#State var width: CGFloat = 0 <-- Initial value MUST BE == .zero
SomeView()
.balanceWidth(store: $width)
AnotherViewRightAligned()
.balanceWidth(store: $width, alignment: .leading)
Sample
struct ContentView: View {
#State var width: CGFloat = 0
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Label 1")
.balanceWidth(store: $width, alignment: .leading)
Text("Textss")
}
HStack {
Text("Label 2: more text")
.balanceWidth(store: $width, alignment: .leading)
Text("Another random texts")
}
HStack {
Text("Label 3: another texts")
.balanceWidth(store: $width, alignment: .leading)
Text("Random texts")
}
}
.padding()
}
}
We can create more relationships between views and balance the widths between them separately by creating more than one #State variable.

Dynamic row height in a SwiftUI form

I'm adding controls to a SwiftUI Form to assist the user enter data (and constrain the entries!). Although there is a lot to like about Forms, I've discovered that things that work nicely outside this container do very unexpected things inside it and it's not always obvious how to compensate for this.
The plan is to have the data field displayed as a single row. When the row is tapped, the control slides out from behind the data field - the row will need to expand (height) to accommodate the control.
I'm using Swift Playgrounds to develop the proof of concept (or failure in my case). The idea is to use a ZStack which will allow a nice sliding animation by overlaying the views and giving them a different zIndex and applying the offset when the data field view is tapped. Sounds simple but of course the Form row does not expand when the ZStack is expanded.
Adjusting the frame of the ZStack while expanding causes all sorts of weird changes in padding (or at least it looks like it) which can be compensated for by counter-offsetting the "top" view but this causes other unpredictable behaviour. Pointers and ideas gratefully accepted.
import SwiftUI
struct MyView: View {
#State var isDisclosed = false
var body: some View {
Form {
Spacer()
VStack {
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
.zIndex(1)
.onTapGesture { self.isDisclosed.toggle() }
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.offset(y: isDisclosed ? 50 : 0)
.animation(.easeOut)
}
}
Spacer()
}
}
}
Collapsed stack
Expanded stack - view overlaps adjacent row
Result when adjusting ZStack vertical frame when expanded - top padding increases
Here is possible solution with fluent row height change (using AnimatingCellHeight modifier taken from my solution in SwiftUI - Animations triggered inside a View that's in a list doesn't animate the list as well ).
Tested with Xcode 11.4 / iOS 13.4
struct MyView: View {
#State var isDisclosed = false
var body: some View {
Form {
Spacer()
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
.zIndex(1)
.onTapGesture { withAnimation { self.isDisclosed.toggle() } }
HStack {
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
}.frame(maxHeight: .infinity, alignment: .bottom)
}
.modifier(AnimatingCellHeight(height: isDisclosed ? 150 : 100))
Spacer()
}
}
}
Use alignmentGuide instead of offset.
...
//.offset(y: isDisclosed ? 50 : 0)
.alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isDisclosed ? 50 : 0) })
...
offset doesn't affect its view's frame. that's why Form doesn't react as expected. On the contrary, alignmentGuide does.
I now have a working implementation using alignment guides as suggested by Kyokook. I have softened the somewhat jarring row height change by adding an opacity animation to the Stepper as it slides out. This also helps to prevent a slightly glitchy overlap of the row title when the control is closed.
struct ContentView: View {
// MARK: Logic state
#State private var years = 0
#State private var months = 0
#State private var weeks = 0
// MARK: UI state
#State var isStepperVisible = false
var body: some View {
Form {
Text("Row 1")
VStack {
// alignment guide must be explicit for the ZStack & all child ZStacks
// must use the same alignment guide - weird stuff happens otherwise
ZStack(alignment: .top) {
HStack {
Text("AGE")
.bold()
.font(.footnote)
Spacer()
Text("\(years) years \(months) months \(weeks) weeks")
.foregroundColor(self.isStepperVisible ? Color.blue : Color.gray)
}
.frame(height: 35) // TODO: Without this, text in HStack vertically offset. Investigate. (HStack align doesn't help)
.background(Color.white) // Prevents overlap of text during transition
.zIndex(3)
.contentShape(Rectangle())
.onTapGesture {
self.isStepperVisible.toggle()
}
HStack(alignment: .center) {
StepperComponent(value: $years, label: "Years", bounds: 0...30, isVisible: $isStepperVisible)
StepperComponent(value: $months, label: "Months", bounds: 0...12, isVisible: $isStepperVisible)
StepperComponent(value: $weeks, label: "Weeks", bounds: 0...4, isVisible: $isStepperVisible)
}
.alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isStepperVisible ? 40 : 0) })
}
}
Text("Row 3")
}
}
}
struct StepperComponent<V: Strideable>: View {
// MARK: Logic state
#Binding var value: V
var label: String
var bounds: ClosedRange<V>
//MARK: UI state
#Binding var isVisible: Bool
var body: some View {
ZStack(alignment: .top) {
Text(label.uppercased()).font(.caption).bold()
.frame(alignment: .center)
.zIndex(1)
.opacity(self.isVisible ? 1 : 0)
.animation(.easeOut)
Stepper(label, value: self.$value, in: bounds)
.labelsHidden()
.alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isVisible ? 25 : 0) })
.frame(alignment: .center)
.zIndex(2)
.opacity(self.isVisible ? 1 : 0)
.animation(.easeOut)
}
}
}
There is still some room for improvement here but on the whole I'm pleased with the result :-)
Thanks to both Kyokook (for putting me straight on offset()) and Asperi.
I think the Kyokook's solution (using AlignmentGuides) is simpler and would be my preference in that it's leveraging Apple's existing API and seems to cause less unpredictable movement of the views in their container. However, the row height changes abruptly and isn't synchronised. The animation in the Asperi's example is smoother but there is some bouncing of the views within the row (it's almost as if the padding or insets are changing and then being reset at the end of the animation). My approach to animation is a bit hit-and-miss so any further comments would be welcome.
Solution 1 (frame consistent, animation choppy):
struct ContentView: View {
#State var isDisclosed = false
var body: some View {
Form {
Text("Row 1")
VStack {
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
.zIndex(1)
.onTapGesture {
self.isDisclosed.toggle()
}
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isDisclosed ? 100 : 0) })
.animation(.easeOut)
Text("Row 3")
}
}
Text("Row 3")
}
}
}
Solution 2 (smoother animation but frame variance):
struct ContentView: View {
#State var isDisclosed = false
var body: some View {
Form {
Text("Row 1")
VStack {
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
.zIndex(1)
.onTapGesture {
withAnimation { self.isDisclosed.toggle() }
}
HStack {
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
}.frame(maxHeight: .infinity, alignment: .bottom)
}
.modifier(AnimatingCellHeight(height: isDisclosed ? 200 : 100))
}
Text("Row 3")
}
}
}
struct AnimatingCellHeight: AnimatableModifier {
var height: CGFloat = 0
var animatableData: CGFloat {
get { height }
set { height = newValue }
}
func body(content: Content) -> some View {
content.frame(height: height)
}
}

Adding Segmented Style Picker to SwiftUI's NavigationView

The question is as simple as in the title. I am trying to put a Picker which has the style of SegmentedPickerStyle to NavigationBar in SwiftUI. It is just like the native Phone application's history page. The image is below
I have looked for Google and Github for example projects, libraries or any tutorials and no luck. I think if nativa apps and WhatsApp for example has it, then it should be possible. Any help would be appreciated.
SwiftUI 2 + toolbar:
struct DemoView: View {
#State private var mode: Int = 0
var body: some View {
Text("Hello, World!")
.toolbar {
ToolbarItem(placement: .principal) {
Picker("Color", selection: $mode) {
Text("Light").tag(0)
Text("Dark").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
}
You can put a Picker directly into .navigationBarItems.
The only trouble I'm having is getting the Picker to be centered. (Just to show that a Picker can indeed be in the Navigation Bar I put together a kind of hacky solution with frame and Geometry Reader. You'll need to find a proper solution to centering.)
struct ContentView: View {
#State private var choices = ["All", "Missed"]
#State private var choice = 0
#State private var contacts = [("Anna Lisa Moreno", "9:40 AM"), ("Justin Shumaker", "9:35 AM")]
var body: some View {
GeometryReader { geometry in
NavigationView {
List {
ForEach(self.contacts, id: \.self.0) { (contact, time) in
ContactView(name: contact, time: time)
}
.onDelete(perform: self.deleteItems)
}
.navigationBarTitle("Recents")
.navigationBarItems(
leading:
HStack {
Button("Clear") {
// do stuff
}
Picker(selection: self.$choice, label: Text("Pick One")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.frame(width: 130)
.pickerStyle(SegmentedPickerStyle())
.padding(.leading, (geometry.size.width / 2.0) - 130)
},
trailing: EditButton())
}
}
}
func deleteItems(at offsets: IndexSet) {
contacts.remove(atOffsets: offsets)
}
}
struct ContactView: View {
var name: String
var time: String
var body: some View {
HStack {
VStack {
Image(systemName: "phone.fill.arrow.up.right")
.font(.headline)
.foregroundColor(.secondary)
Text("")
}
VStack(alignment: .leading) {
Text(self.name)
.font(.headline)
Text("iPhone")
.foregroundColor(.secondary)
}
Spacer()
Text(self.time)
.foregroundColor(.secondary)
}
}
}
For those who want to make it dead center, Just put two HStack to each side and made them width fixed and equal.
Add this method to View extension.
extension View {
func navigationBarItems<L, C, T>(leading: L, center: C, trailing: T) -> some View where L: View, C: View, T: View {
self.navigationBarItems(leading:
HStack{
HStack {
leading
}
.frame(width: 60, alignment: .leading)
Spacer()
HStack {
center
}
.frame(width: 300, alignment: .center)
Spacer()
HStack {
//Text("asdasd")
trailing
}
//.background(Color.blue)
.frame(width: 100, alignment: .trailing)
}
//.background(Color.yellow)
.frame(width: UIScreen.main.bounds.size.width-32)
)
}
}
Now you have a View modifier which has the same usage of navigationBatItems(:_). You can edit the code based on your needs.
Usage example:
.navigationBarItems(leading: EmptyView(), center:
Picker(selection: self.$choice, label: Text("Pick One")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
}, trailing: EmptyView())
UPDATE
There was the issue of leading and trailing items were violating UINavigationBarContentView's safeArea. While I was searching through, I came across another solution in this answer. It is little helper library called SwiftUIX. If you do not want install whole library -like me- I created a gist just for navigationBarItems. Just add the file to your project.
But do not forget this, It was stretching the Picker to cover all the free space and forcing StatusView to be narrower. So I had to set frames like this;
.navigationBarItems(center:
Picker(...) {
...
}
.frame(width: 150)
, trailing:
StatusView()
.frame(width: 70)
)
If you need segmentcontroll to be in center you need to use GeometryReader, below code will provide picker as title, and trailing (right) button.
You set up two view on the sides left and right with the same width, and the middle view will take the rest.
5 is the magic number depends how width you need segment to be.
You can experiment and see the best fit for you.
GeometryReader {
Text("TEST")
.navigationBarItems(leading:
HStack {
Spacer().frame(width: geometry.size.width / 5)
Spacer()
picker
Spacer()
Button().frame(width: geometry.size.width / 5)
}.frame(width: geometry.size.width)
}
But better solution is if you save picker size and then calculate other frame sizes, so picker will be same on ipad & iphone
#State var segmentControllerWidth: CGFloat = 0
var body: some View {
HStack {
Spacer()
.frame(width: (geometry.size.width / 2) - (segmentControllerWidth / 2))
.background(Color.red)
segmentController
.fixedSize()
.background(PreferenceViewSetter())
profileButton
.frame(width: (geometry.size.width / 2) - (segmentControllerWidth / 2))
}
.onPreferenceChange(PreferenceViewKey.self) { preferences in
segmentControllerWidth = preferences.width
}
}
struct PreferenceViewSetter: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: PreferenceViewKey.self,
value: PreferenceViewData(width: geometry.size.width))
}
}
}
struct PreferenceViewData: Equatable {
let width: CGFloat
}
struct PreferenceViewKey: PreferenceKey {
typealias Value = PreferenceViewData
static var defaultValue = PreferenceViewData(width: 0)
static func reduce(value: inout PreferenceViewData, nextValue: () -> PreferenceViewData) {
value = nextValue()
}
}
Simple answer how to center segment controller and hide one of the buttons.
#State var showLeadingButton = true
var body: some View {
HStack {
Button(action: {}, label: {"leading"})
.opacity(showLeadingButton ? true : false)
Spacer()
Picker(selection: $selectedStatus,
label: Text("SEGMENT") {
segmentValues
}
.id(UUID())
.pickerStyle(SegmentedPickerStyle())
.fixedSize()
Spacer()
Button(action: {}, label: {"trailing"})
}
.frame(width: UIScreen.main.bounds.width)
}

How to prevent a Spacer to make a VStack greedly grow beyond necessary?

I roughly have:
var body: some View
{
HStack(alignment: .top) {
AvatarView()
MessageBubble()
if message.isDeleted != true
{
VStack {
Button(action: {
// ...
}) {
Image(systemName: "chevron.down")
}
Spacer() // THIS SPACER
Button(action: {
// ...
}) {
Text("😀")
}
}
}
}
}
The root HStack~'s height is dictated by the sizeMessageBubblewhich is always taller than itsAvatarViewandVStack` siblings.
The problem is when I add a Spacer inside the VStack, then the whole HStack incredibly grows. The Spacer is "greedly" making everything grow with no limits.
I do want to have a button at the very top and the other at the very bottom of the VStack while being limited to MessageBubble's height.
How could I make it grow no more than MessageBubble?
If I understood correctly you expected something like this (elements alignment)
Here is demo code (frames for coloured rects are not important here, sizes just for demo, you will place your views instead there):
struct TestAlignmentSize: View {
#State var height: CGFloat = 40
var body: some View {
HStack(alignment: .top, spacing: 20) {
Rectangle()
.fill(Color.blue)
.frame(width: 50, height: 50)
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 200)
.alignmentGuide(.top, computeValue: { d in // [!!]
self.height = d.height
return d[.top]
})
VStack {
Button(action: {}) {
Image(systemName: "chevron.down")
}
Spacer()
Button(action: {}) {
Text("Button")
}
}.frame(height: self.height) // [!!]
}
}
}