SwiftUI - Resizable List height that dependent on an element count - swiftui

I have some troubles with dynamically changing List height that dependent on elements count.
I tried this solution but it didn't work.
List {
ForEach(searchService.searchResult, id: \.self) { item in
Text(item)
.font(.custom("Avenir Next Regular", size: 12))
}
}.frame(height: CGFloat(searchService.searchResult.count * 20))

TL;DR
This is not how the designers of SwiftUI want you to use lists. Either you will have to come up with a hacky solution that will probably break in the future (see below), or use something other than a list.
Background
SwiftUI tends to have two types of Views
Those designed to be easily modifiable and composable, providing unlimited customizability for a unique look and feel.
Those designed to provide a standard, consistent feel to some type of interaction, regardless of what app they are used in.
An example of type 1 would be Text. You can change font size, weight, typeface, color, background, padding, etc. It is designed for you to modify it.
An example of type 2 would be List. You are not in direct control of row height, you can't change the padding around views, you can't tell it to show only so many rows, etc. They don't want it to be very customizable, because then each app's lists would behave differently, defeating the purpose of a standard control.
List is designed to fill the entire parent View with as many rows as possible, even if they are empty or only partially on screen (and scroll if there are too many to show at once).
Your issue
The problem you are having comes about because the size of the List does not affect the size of its rows in any way. SwiftUI doesn't care if there are too many or too few rows to fit in your preferred size; it will happily size its rows according to content, even if it means they don't all show or there are empty rows shown.
If you really need rows to resize according to the size of their parent, you should use a VStack. If it needs to scroll, you will need to wrap the VStack in a ScrollView.
Hacky solution
If you still insist on using a list, you will have to do something like the following:
struct ContentView: View {
#State private var textHeight: Double = 20
let listRowPadding: Double = 5 // This is a guess
let listRowMinHeight: Double = 45 // This is a guess
var listRowHeight: Double {
max(listRowMinHeight, textHeight + 2 * listRowPadding)
}
var strings: [String] = ["One", "Two", "Three"]
var body: some View {
VStack {
HStack {
Text(String(format: "%2.0f", textHeight as Double))
Slider(value: $textHeight, in: 20...60)
}
VStack(spacing: 0) {
Color.red
List {
ForEach(strings, id: \.self) { item in
Text(item)
.font(.custom("Avenir Next Regular", size: 12))
.frame(height: CGFloat(self.textHeight))
.background(Color(white: 0.5))
}
}
// Comment out the following line to see how List is expected to work
.frame(height: CGFloat(strings.count) * CGFloat(self.listRowHeight))
Color.red
}.layoutPriority(1)
}
}
}
The slider is there to show how the list row heights change with the height of their child view. You would have to manually pick listRowPadding and listRowMinHeight to get the appearance that best matches your expectation. If Apple ever changes how a List looks (changes padding, minimum row heights, etc.) you will have to remember to come back and adjust these values manually.

Self size List:
If you want a List to show it's content all at once, It means you don't need the recycling feature (the key feature of the list), So all you need is to not using a List! Instead, you can use ForEach directly, then it will size itself based on it's content:
ForEach(searchService.searchResult, id: \.self) { item in
VStack(alignment: .leading) {
Text(item).font(.custom("Avenir Next Regular", size: 12))
Divider()
}.padding(.horizontal, 8)
}
You can change all sizes and spacings according to your needs
Note that You can use LazyVStack from iOS 14 to make it lazy-load and boost its performance.

Starting from iOS 14 you can use LazyVStack instead of List.
List seems to span entire parent view height independent of rows height or count.
LazyVStack {
ForEach(searchService.searchResult, id: \.self) { item in
Text(item)
.font(.custom("Avenir Next Regular", size: 12))
}
}.frame(height:
Other solution is to set .frame(height: ) on List based on rowCount*rowHeight or other GeometryReader -> geometry.size.height

SwiftUi has evolved. Here's a plain and simple answer for SwiftUI 3: https://stackoverflow.com/a/65769005/4514671

Related

Displaying 2 Columns in SwiftUI List (pulling data from Core Data)

I have created a list in SwiftUI where it is pulling in info from 2 attributes from core data (title and month).
Works great, but I'm sure you'll agree a not very elegant solution in using string interpolation with a load of spaces in the middle to separate. Looks horrible.
How do i amend the code below to create a 2 column list with "title" going in the first column and "month" the second please (obviously in the same row).
List {
ForEach(toDos) {
listedToDos in
Text ("\(listedToDos.title!) Expires: \(listedToDos.month!)")
}
.onDelete(perform: deleteItems)
}
You can just use HStack for each row in your list.
ForEach ... {
HStack{
Text("\(listedToDos.title!)")
Text("Expires: \(listedToDos.month!)")
Spacer()
}
}
Give your Text items a fixed frame width if you want the columns to be left aligned. Use padding to create some whitespace.
Text("\(listedToDos.title!)")
.frame(width: 150, alignment: .leading)
.padding(.leading, 10)
How about:
ForEach(toDos) { listedToDos in
HStack{
Text ("\(listedToDos.title!)")
Spacer()
Text("Expires: \(listedToDos.month!)")
}
}
This will place the first Text on the left and the second to the right.

SwiftUI - How to perfectly align multiple Text with different font sizes in VStack?

I have 2 Text elements in a VStack. They are set to different font size.
How do I get the first glyph of the 2 Text elements to align perfectly left? Is that even possible?
Below is the code snippet:
extension Font {
static let countDownTitle: Font = Font.system(size: 46, weight: .regular, design: .rounded).leading(.tight)
}
struct MyView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello!!")
Text("20.49").font(.countDownTitle)
}
}
}
You have a bit of an artificial test going on here. Remember, each character takes up a different amount of room in a proportional font takes up a different amount of room. When they are laid out, they are put in a space that is controlled by the font designer, not us. You can see this is you cycle through different numbers. The 4 is pretty much right on, but the 5 is way off. This is one of those things that your ability to control it is lacking.
While, I am not at all recommending this, you could get perfect alignment like this, using a monospaced font and an .offset():
extension Font {
static let countDownTitle: Font = Font(UIFont.monospacedSystemFont(ofSize: 46, weight: .regular))
}
struct MyView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello!!")
Text("50.49").font(.countDownTitle)
.offset(x: -2.2, y: 0)
}
}
}
But, this is really ugly.

SwiftUI: HStack, setting custom spacing between two views in stack other than the standard spacing on HStack?

I know I can set the spacing between views in an HStack like this:
HStack(spacing: 10) { .. }
If I had lets say 8 views in the HStack, is there a way to set the spacing to 1 only between two consecutive views of the 8 views in the stack overriding the initial HStack spacing set to 10 in this case?
I'd like all views in the stack to be 10 apart, but, two of the views I want to be 1 apart from each other.
I know I could put another HStack containing the two views and setting the spacing the sub stack to 1. I'm wondering if there's another way that might be better.
Maybe use padding instead? But "I know I could put another HStack containing the two views and setting the spacing the sub stack to 1" is probably the best way.
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
ForEach(0..<10) { index in
Rectangle()
.fill(Color.green)
.frame(width: 40, height: 40)
.padding(.horizontal, index == 0 || index == 1 ? 1 : 10)
}
}
}
}
Well, what are you expecting as "another way"?
You have a main HStack where you can put VStacks, HStacks and ZStacks in.
So its Like..
HStack(spacing: 10) {
HStack{}
HStack{}
HStack(spacing:1){
HStack{}
}
}
Dont get me wrong but I dont understand what your question is. This looks quite comfortable for me :-)
Greetings.

Get View Size inside Grouped List in SwiftUI

I'm currently using a List with a GroupedListStyle(), and with a .regular horizontalSizeClass (e.g an iPad in landscape mode), this style of List automatically creates some padding to each section, like this:
struct ListView: View {
var body: some View {
List {
Section {
Text("One")
Text("Two")
Text("Three")
}
}.listStyle(GroupedListStyle())
}
}
This is great, I want this. But since I need to display some elements inside the rows with a fixed width, I need to know the actual width of the row inside the List.
I've tried wrapping the List with a GeometryReader, but the width doesn't match the row's width because of the padding around the entire Section.
struct ListView: View {
var body: some View {
GeometryReader { geometry in
List {
Section {
Text("One") // <-- geometry.size.width != Text.width
Text("Two")
Text("Three")
}
}.listStyle(GroupedListStyle())
}
}
}
So I've tried using the PreferenceKey method explained here by putting a GeometryReader in the listRowBackground modifier of the row, but the value is never updated.
Is there a working way to get the size of the parent, without using a GeometryReader around the parent (as it is finnicky on a List, and needs to be specified Height and Width to work properly)?
I've had to resort to a UITableView with style .insetGrouped, implemented with a Representable and used the property layoutMargins that gives exactly the size of the horizontal margins in this size class.

How can I select a SwiftUI Text view, or string, based on available space?

For example, if my UI needed to display a length Measurement in human readable form, it might want to choose from one of the following formats to display one inch:
1"
1 in
1 inch
one inch
So far I have tried:
truncationMode(_:): only accepts positional argument, no option for custom truncation
GeometryReader: tells me what space is available (super useful!) but I don't see how to dynamically select a dynamically sized sub-view, seems to be optimized for generating fixed sized sub-views or overflowing the position
When I try to find another app that might have solved this problem it seems that they all rearrange the layout on orientation or other size change. I want to continue to have a single HStack of Text views that fit the space, keeping all the important information from being truncated when possible.
Let's define this View:
struct FlexibleTextView: View {
let possibleTexts: [String]
var body: some View {
GeometryReader { geometry in
Text(self.possibleTexts.last(where: { $0.size(withAttributes: [.font: UIFont.systemFont(ofSize: 17.0)]).width < geometry.size.width }) ?? self.possibleTexts[0])
.lineLimit(1)
}
}
init(_ possibleTexts: [String]) {
self.possibleTexts = possibleTexts.sorted {
$0.size(withAttributes: [.font: UIFont.systemFont(ofSize: 17.0)]).width < $1.size(withAttributes: [.font: UIFont.systemFont(ofSize: 17.0)]).width
}
}
}
When you init it, the possible texts are automatically sorted by their actual width. It takes the last one (so the one width the greatest width) where the width is smaller than the width of the container, which we get from GeometryReader. If even the first, so the smallest text is to big, (so .last(where: { ... }) will return nil), we still use that first text, but you could also change this yourself to whatever you would like.
Here's an interactive example:
struct ContentView: View {
#State var width: CGFloat = 80
var body: some View {
VStack {
FlexibleTextView(["1\"", "1 in", "1 inch", "one inch"])
.frame(width: width, height: 17)
.border(Color.red)
Slider(value: $width, in: 10 ... 80)
}
.padding()
}
}
With the slider, you can adjust the width to see the effect.