23

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))
Abjox
  • 579
  • 1
  • 4
  • 14
  • So, your idea is to set List height (the height of the list view itself) to 20 px per item. If it worked, it wouldn't be scrollable in this case (since there's enough space for all the elements). Why not just use VStack then? If, on the other hand, your goal is to set size for items of the list, I assume you'd need to set size of the items, not the list. I'm not very familiar with SwiftUI though. – FreeNickname Sep 23 '19 at 10:46
  • no, I don't need to change the row height. I need to change heigh of a List view, that dependent on an element count. For example, if I have only one element on the array -> I need to show List with only one row and this row will be the whole frame of this list – Abjox Sep 23 '19 at 10:54
  • 1
    So, is there any particular reason why you'd like to use `List` and not `VStack`, if you're not going to use scrolling? – FreeNickname Sep 23 '19 at 11:50
  • Your solution should work - is your `searchService` marked as `@ObservedObject`? And `searchResult` marked as `@Published`? – Michcio Sep 23 '19 at 12:48
  • @Michcio `searchService` marked as `@ObservedObject`, but also it inherited from `NSObject` for have ability conform to `MKLocalSearchCompleter` delegate and at this case, `@Published` didn't work. And for having ability to be notified at changes happens I use `ObservableObjectPublisher`. – Abjox Sep 23 '19 at 13:52
  • @FreeNickname Yes, I'm using `VStack` now, but in case of a large number of elements, I want to have the ability to scroll my List. – Abjox Sep 23 '19 at 13:55
  • 1
    @Abjox consider adding some proxy object for `MKLocalSearchCompleter` and remove `NSObject` - it should start working :) – Michcio Sep 23 '19 at 13:58

4 Answers4

27

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

  1. Those designed to be easily modifiable and composable, providing unlimited customizability for a unique look and feel.
  2. 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.

John M.
  • 8,892
  • 4
  • 31
  • 42
21

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.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 4
    OMG. Thank you! List has caused me endless trouble - if you use 'List' in a child view, then expect your height to shrink to zero. Read loads of hacks trying to use GeometryReader to get child content size, but couldn't get it working. Switching from List to ForEach instantly fixed it for me. Thanks. – RichS Apr 11 '20 at 10:21
  • 1
    One of the other key features of List is the ability to have an `Editmode` instantly without any code. How would you archive that when using VStack/ScrollView? – Peacemoon Aug 17 '20 at 10:02
  • 2
    Just a note, this answer loses an essential part of List, that it doesn't render everything at once. If you want to do this on a List that has hundreds of items, you'll have performance issues with scrolling. – iSpain17 Sep 15 '20 at 19:27
  • 1
    @iSpain17 self size list doesn't seem to require that kind of performance as I mentioned in the very beggining of my answer, Since more data leads it to no. be a self-size any more. But **from iOS 14**, you can use a `LazyVStack` instead and that is the exact thing you are looking for. – Mojtaba Hosseini Sep 15 '20 at 19:41
  • 2
    I think the OP wants something that shrinks below a threshold and scrolls above a threshold. I'm looking for a similar thing, and with that, you just have to use a .frame(maxHeight:xxx) with xxx calculated by the lesser of ( % of screen or parent view height ) or ( elements.height * thresholdElementCount ). – ChrisH Mar 15 '21 at 20:48
  • Problem here is if you have to bind values, the ForEach will run every time you update any value. List has a way to handle updating values only for that particular index. – Sanjeevcn Dec 24 '21 at 06:08
  • Would a more efficient approach be to wrap the `ForEach` *with* a `VStack`, then just add the `Text` and `Dividers` directly, rather than creating tons of interim`VStack`s inside the `ForEach`? (I'm actually wondering if you even need the `VStack` at all but am not in a place to test this.) – Mark A. Donohoe Nov 18 '22 at 01:20
8

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

Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143
-5

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

Rebeloper
  • 813
  • 10
  • 22