Having multiline text in a horizontal scroll view in SwiftUI - swift

I want to make a UI that loads an array of strings into a scrollable horizontal stack. The texts however should have a max width of 100, and expand to be multiline to fill the space. This works, however the text is cut off and the horizontal ScrollView is only allowing the height for one line of text.
How do I let the ScrollView expand vertically to whatever the largest text in the stack is?
I want to have a control right above the stack, so setting the height to .infinity isn't a solution, and preferably don't want to have to work with GeometryReader.
My code so far:
VStack {
Capsule()
.foregroundColor(.black.opacity(0.5))
.frame(width: 48, height: 2)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(chats, id: \.self) { chat in
Text(chat)
}
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: 100)
}
}
}
The result:

Change .frame(maxWidth: 100) to .frame(width: 100). You'll then need to put a maxHeight on your HStack lest it grow too tall.
VStack {
Capsule()
.foregroundColor(.black.opacity(0.5))
.frame(width: 48, height: 2)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top) {
ForEach(chats, id: \.self) { chat in
Text(chat)
.frame(width: 100)
}
}
.frame(maxHeight: 200, alignment: .top)
}
}
Result:

Related

Override HStack alignment for one child

Is there a way to dynamically override the alignment property of an HStack in an individual element?
Consider this scenario
There is a parent HStack with alignment = bottom
There are 3 elements inside the HStack of different sizes
I want the 3rd element to align to the top of the HStack. This alignment is different from the Hstack's bottom alignment
var body: some View {
HStack(alignment: .bottom) {
Rectangle()
.fill(.yellow)
.frame(height: 100)
Rectangle()
.fill(.blue)
.frame(height: 20)
// I want this to go to the top of the HStack
Rectangle()
.fill(.green)
.frame(height: 50)
}
.background {
Color.red
}
}
I'm trying to get the HStack to respect the highest height of 100 and just alter the last element's alignment.
I've tried wrapping the 3rd element in another stack but that only works if I specify a maxHeight equal to the tallest height among the parent's children, 100.
This means these rectangles have to know about their sibling elements.
HStack {
Rectangle()
.fill(.green)
.frame(height: 50)
}
.frame(maxHeight: 100, alignment: .top)
You could try this:
var body: some View {
HStack(alignment: .bottom) {
Rectangle()
.fill(.yellow)
.frame(height: 100)
Rectangle()
.fill(.blue)
.frame(height: 20)
VStack {
Rectangle()
.fill(.green)
.frame(height: 50)
Spacer()
}
}
.background {
Color.red
}
}
If, for some reason, you want to limit the range of how much space can the Spacer take up, you can add a modifier as per following example:
VStack {
Rectangle()
.fill(.green)
.frame(height: 50)
Spacer()
.frame(minHeight: 10, maxHeight: 50)
}

Different verticalAlignment in HStack in SwiftUI

I'm trying to create simple cell layout in SwiftUI but I somehow stumbled on problem how to define different vertical alignments of elements in same HStack:
This is basically what I'm trying to achieve:
Whole view should be a cell, where there are some arbitrary paddings(24 on top, 20 at bottom). What is important is following:
HStack contains icon (red), vstack (title and description) and another icon (green)
Red icon should be aligned to the top of the HStack as well as vstack with texts
Green icon should be centered in the whole view
I've tried to achieve this with following code:
VStack(spacing: 0) {
HStack(alignment: .top, spacing: 24) {
Image(nsImage: viewModel.icon)
.frame(width: 20.0, height: 20.0)
VStack(alignment: .leading, spacing: 4) {
Text(viewModel.title)
Text(viewModel.text)
}
Spacer()
Image(nsImage: "viewModel.next")
}
.padding([.top], 24)
.padding([.bottom], 20)
Divider()
}
Without luck as obviously the green icon is also aligned to the top. I've tried to mess around with layout guides without success.
Another solution I've tried is
VStack(spacing: 0) {
HStack(alignment: .top, spacing: 24) {
Image(nsImage: viewModel.icon)
.frame(width: 20.0, height: 20.0)
VStack(alignment: .leading, spacing: 4) {
Text(viewModel.title)
Text(viewModel.text)
}
Spacer()
VStack {
Spacer()
Image(nsImage: "viewModel.next")
Spacer()
}
}
.padding([.top], 24)
.padding([.bottom], 20)
Divider()
}
which doesn't work either as I have more of these 'cells' in super view and their height is stretched to fill the superview.
Any idea how to achieve this?
I would treat the left-hand image and text as a single, top-aligned HStack, then put that in another HStack aligned centrally with the right-hand image. In shorthand, omitting spacing etc.:
HStack(alignment: .center) {
HStack(alignment: .top) {
Image(nsImage: ...)
VStack(alignment: .leading) {
Text(...)
Text(...)
}
}
Spacer()
Image(nsImage: ...)
}
That way, you only have a spacer working in the horizontal axis, so your overall vertical frame will be determined by the content alone.

SwiftUI - LazyVGrid spacing

I have a LazyVGrid like so in SearchView.swift file
let layout = [
GridItem(.flexible()),
GridItem(.flexible())
]
NavigationView {
ZStack {
VStack {
....Some other stuff here
ScrollView (showsIndicators: false) {
LazyVGrid(columns: layout) {
ForEach(searchViewModel.allUsers, id: \.uid) { user in
NavigationLink(destination: ProfileDetailedView(userData: user)) {
profileCard(profileURL: user.profileURL, username: user.username, age: user.age, country: user.city)
}
}
}
}
}
}
}
My profileCard.swift looks like:
var body: some View {
ZStack {
Image.image(urlString: profileURL,
content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
}
)
.frame(width: 185, height: 250)
.overlay(
LinearGradient(gradient: Gradient(colors: [.clear, .black]), startPoint: .center, endPoint: .bottom)
)
.overlay(
VStack (alignment: .leading) {
Text("\(username.trimmingCharacters(in: .whitespacesAndNewlines)), ")
.font(.custom("Roboto-Bold", size:14))
.foregroundColor(Color.white)
+ Text("\(age)")
.font(.custom("Roboto-Light", size:14))
.foregroundColor(Color.white)
HStack {
Text("\(country)")
.font(.custom("Roboto-Light", size:14))
.foregroundColor(.white)
}
}
.padding(.leading, 15)
.padding(.bottom, 15)
,alignment: .bottomLeading
)
.cornerRadius(12)
}
}
This is producing 2 different spaces on different screen sizes:
iPhone 12:
iPhone 12 Pro Max
Im trying to get the same amount of spacing between the cards (horizontal and verticle) and the around the cards on all devices, any help on achieving this?
UPDATE
Following the example by #Adrien has gotten me closer to the problem, but when I use an image, the results change completely
let columns = Array(repeating: GridItem(.flexible(), spacing: 20, alignment: .center), count: 2)
ScrollView (showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(searchViewModel.allUsers, id: \.uid) { user in
NavigationLink(destination: ProfileDetailedView(userData: user)) {
HStack {
Image("placeholder-avatar")
.resizable()
.aspectRatio(contentMode: .fill)
}
.frame(maxWidth: .infinity, minHeight: 240) // HERE 1
.background(Color.black)
.cornerRadius(25)
}
}
}.padding(20)
}
The array of GridItem only fixes the behavior of the container of each cell. Here it is flexible, but that does not mean that it will modify its content.
The View it contains can have three different behaviors :
it adapts to its container:
like Rectangle, Image().resizable(), or any View with a .frame(maxWidth: .infinity, maxHeight: .infinity)) modifier.
it adapts to its content (like Text, or HStack).
it has a fixed size (as is your case with .frame(width: 185, height: 250))
If you want the same spacing (vertical and horizontal) between cells, whatever the device, the content of your ProfileDetailedView must adapt to its container :
you have to modify your cell so that it adopts behavior 1.
you can use the spacing parameters of yours GridItem (horizontal spacing) and LazyVGrid (vertical).
Example:
struct SwiftUIView5: View {
let columns = Array(repeating: GridItem(.flexible(), spacing: 20, alignment: .center), count: 2) // HERE 2
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) { // HERE 2
ForEach((1...10), id: \.self) { number in
HStack {
Text(number.description)
.foregroundColor(.white)
.font(.largeTitle)
}
.frame(maxWidth: .infinity, minHeight: 200) // HERE 1
.background(Color.black)
.cornerRadius(25)
}
}
.padding(20) // HERE 2
}
}
}
Actually, the spacing is a function of the screen size, in your case. You have hard-coded the size of the card (185x250), so the spacing is as expected, a bit tighter on smaller screens, bit more airy on larger screens.
Note that the vertical spacing is consistent and as expected. This seems logical because there is no vertical size constraint.
BTW, the two screenshots are not the same size, otherwise this aspect would be more obvious.
If you want to keep the size of the card fixed, you can perhaps at least keep the horizontal space between them constant, but then you will have larger margins on the sides.
Or you can adjust the size of the card dynamically. Check out GeometryReader, which will help you access runtime screen sizes.

Prevent view from overflowing in both directions

I'm trying to build a user-expandable view that will show more of its content by increasing its height. To accomplish this I will add .clipped() so that the content shown out of its bounds will be hidden, just like you would add overflow: hidden; in CSS.
However, it seems like by default VStack is centering its children, so when the height is smaller than the sum of the children's height, they overflow in both the top and the bottom.
Here is an example of what I'm talking about:
struct ExandableView: View {
var body: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.red)
.frame(height: 50)
.padding(.horizontal)
Rectangle()
.fill(Color.green)
.frame(height: 50)
.padding(.horizontal)
}
.frame(height: 90)
.background(Color.blue)
}
}
Is there any way to make it behave so that the red item (the first) is always inside the blue container and the children can only overflow from the bottom?
Change frame alignment according to ExandableView. like this
struct ExandableView: View {
var body: some View {
// Other VStack code
}
.frame(height: 90, alignment: isExpand ? .top : .center) //<--Here
.background(Color.blue)
}
}

How can I create an "unfilled frame" in SwiftUI?

I'm sure this is something super-simple, but I cannot seem to figure it out. I am trying to create a "widget" that consists of three lines of text, stacked vertically. This information should be placed inside a "frame" or "border" so it resembles a card. There will be a row of these cards that will scroll horizontally.
Believe it or not, the only part of this I cannot figure out is how to draw the border around the widget. I've tried .border, but that snugs the border right up against the text. I know I can add padding, but what I really need is a fixed-size card so each element in the scrolling list is identical.
I've come closest using this:
.frame(width: geometry.size.width/1.3, height: 200)
.background(Color.white)
.border(Color.blue)
.cornerRadius(20)
...but the corners are all clipped. For reference, here's the complete code listing:
struct AccountTile: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading, spacing: 8) {
Text("Account Balance").font(.largeTitle)
Text("Account Name").font(.headline)
HStack(spacing: 0) {
Text("There are ").font(.caption).foregroundColor(.gray)
Text("6 ").font(.caption).fontWeight(.bold).foregroundColor(.blue)
Text("unreconciled transactions.").font(.caption).foregroundColor(.gray)
}
}
.frame(width: geometry.size.width/1.3, height: 200)
.background(Color.white)
.border(Color.blue)
.cornerRadius(20)
}
}
}
...and here's what that code is producing:
This is almost what I'm looking for - I just need the border to be complete.
Use some padding and overlay to create your border. Here is the code (:
struct AccountTile: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading, spacing: 8) {
Text("Account Balance").font(.largeTitle)
Text("Account Name").font(.headline)
HStack(spacing: 0) {
Text("There are ").font(.caption).foregroundColor(.gray)
Text("6 ").font(.caption).fontWeight(.bold).foregroundColor(.blue)
Text("unreconciled transactions.").font(.caption).foregroundColor(.gray)
}
}.frame(width: geometry.size.width/1.3, height: 200)
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.blue, lineWidth: 2))
}
}
}