SwitUI - VStack - Jumping when text entered - swiftui

I am developing a basic passcode entry screen, consisting off a top Stack to display currently entry, then some HStack's displaying the numbers
VStack(){
HStack(spacing: 20){
ForEach(codes,id: \.self){i in
Text("*")
}
}
HStack(){
<Number 1 - 3>
}
HStack(){
<Number 4 - 6>
}
HStack(){
<Number 7 - 9>
}
HStack(){
<Number 0>
}
}
This issue im facing is when there is no passcode entered the top HStack dosnt use up any space, so has a vertical height of 0, when I enter a code, it forces the whole view to jump a little as the view resizes.
How can I stop that so

If I'm being honest, it was quiet fun to build ! 😄 Don't forget to mark this answer as the right one if it solved your issue. ✅
PROBLEM
The jumping effect is due to SwiftUI updating all views positions based on available space calculated based on your content (passcode digits). The font, font weight, text size, etc… all has an effect on the available space left for other views.
SOLUTION
To avoid that, you need to a predefined frame that will let the parent view know that your digits will never take more space. Doing so, each update won't effect the position of any other view because the allocated top space would always be size you specified and not the digits sizes (or absence).
CODE
import SwiftUI
import Combine
// Using Combine to manage digits and future network calls…
class PasscodeManager: ObservableObject {
let codesQuantity = 4
#Published var codes = [Int]()
}
struct PasscodeView: View {
#StateObject private var manager = PasscodeManager()
var body: some View {
VStack {
Spacer()
// Dots placeholders and passcode digits
selectedCodes
Spacer()
// Numberpad
PasscodeLine(numbers: 1...3) { add(number: $0) }
PasscodeLine(numbers: 4...6) { add(number: $0) }
PasscodeLine(numbers: 7...9) { add(number: $0) }
PasscodeLine(numbers: 0...0) { add(number: $0) }
Spacer()
}
.padding()
}
var selectedCodes: some View {
let minDots = manager.codes.count == manager.codesQuantity ? 0:1
let maxDots = manager.codesQuantity - manager.codes.count
return HStack(spacing: 32) {
ForEach(manager.codes, id: \.self) { Text("\($0)") }
if maxDots != 0 {
ForEach(minDots...maxDots, id: \.self) { _ in
Circle().frame(width: 12)
}
}
}
.font(.title.bold())
// Setting a default height should fix your problem. 🙂
.frame(height: 70)
}
func add(number: Int) {
guard manager.codes.count < manager.codesQuantity else { return }
manager.codes.append(number)
}
}
struct PasscodeLine: View {
let numbers: ClosedRange<Int>
var select: (Int) -> Void
var body: some View {
HStack {
ForEach(numbers, id: \.self) { number in
Spacer()
Button(action: { select(number) },
label: {
Text("\(number)")
.font(.title)
.fontWeight(.medium)
.foregroundColor(Color(.label))
.padding(32)
.background(Color(.quaternarySystemFill))
.clipShape(Circle())
})
}
Spacer()
}
}
}
RESULT

Related

SwiftUI migrate NavigationView to NavigationStack or something

I'm trying to take my app and migrate my opening NavigationView which has been deprecated to the new NavigationStack.
My opening screen looks like:
and the code that presents it is:
import SwiftUI
struct MainAppView: View {
#State var listSelection: Int = 1
#Binding var isLoggedIn: Bool
var body: some View {
NavigationView {
if DBManager.shared.checkDatabaseFileExists()
{
SideListView(listSelection: $listSelection)
HStack {
if listSelection == 1
{
AccountsListView()
}
if listSelection == 2
{
SetAsideBalanceListView()
}
if listSelection == 3
{
BankBalanceListView()
}
if listSelection == 4
{
SetupMenuView()
}
if listSelection == 5
{
ReportsListView()
}
} // END HSTACK
} else {
Text("NO DATABASE FOUND!!!!")
.font(.system(size: 60))
} // END OUTTER IF/ELSE DATABASE FOUND
} // END NAVIGATION VIEW
} // END BODY VIEW
} // END STRUCT VIEW
I did the simple thing of changing NavigationView to NavigationStack and while it compiles, it looks wrong:
Chris... the solution that I implemented using your suggestion is working and would be acceptable, except that the navigation behavior seems to have changed with NavigationSplitView and NavigationStack. When a selection is made in the left pane the corresponding view appears in the right pane. The view in the right pane has a NavigationLink to a subview. This works and the subview has the back button. What I noticed is that with NavigationView if the user in a subview clicks on the selection in the left pane, the view immediately pops the appropriate selected view to the right pane clearing the subview of some other selection that is showing. But in this model, using NavigationStack on the selected view, if the subview is showing for a given selection, clicking on the left pane selection has no effect until the existing subview is back buttoned to the parent view at which time the selected view is presented.
Like this:
And then selected the sub view looks OK like this:
But when I select in the left pane another selection like this:
Using NavigationView the subview for SetAside would immediately pop but instead only shows after I use the back button on the sub view...
UPDATES:
Here is the code I've implemented for the MainAppView.swift
import SwiftUI
struct MainAppView: View {
#State var listSelection: Int? = 1
#Binding var isLoggedIn: Bool
var body: some View {
if DBManager.shared.checkDatabaseFileExists()
{
NavigationSplitView {
SideListView(listSelection: $listSelection)
} detail: {
NavigationStack {
switch listSelection {
case 1: AccountsListView()
case 2: SetAsideBalanceListView()
case 3: BankBalanceListView()
case 4: SetupMenuView()
case 5: ReportsListView()
default: Text("Select View")
}
} // END NAVIGATION STACK
} // END NAVIGATION SPLIT VIEW DETAIL
} else {
Text("NO DATABASE FOUND!!!!")
.font(.system(size: 60))
} // END OUTTER IF/ELSE DATABASE FOUND
} // END BODY VIEW'
} // END STRUCT VIEW
Here is the view code for the SetAsideBalanceListView you can comment out the DB function calls and get an idea of the code. They are all somewhat clones of each other
import SwiftUI
struct SetAsideBalanceListView: View {
var accounts: [Accounts.AccountRecord] {
var acctRec = [Accounts.AccountRecord]()
acctRec = Accounts.shared.selectAllAccounts()
return acctRec
}
var body: some View {
// NavigationStack {
VStack {
CustomDivider(horizontalpadding: 0, thickness: 1)
ForEach (accounts, id: \.self) { accountRec in
let result = Budget.shared.getBudgetMonthAndYearForView(withAccountCode: accountRec.account_code)
NavigationLink(destination: SetAsideBalanceView(account_code: accountRec.account_code, currentBudgetYear: result.viewYear)){
HStack {
Image(systemName: "dollarsign.circle")
.foregroundColor(.white)
.background(Color("iconBlue"))
.cornerRadius(25)
.padding(.leading, 15)
Text(accountRec.account_name)
Spacer()
}
}
.foregroundColor(.primary)
Divider().background(Color.primary)
}// END FOR EACH
} // END VSTACK
.font(.title2)
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(.top, 20)
.navigationTitle("Managing Your Money - Set Aside")
// } // END NAVIGATION STACK
} // END BODY VIEW
} // END STRUCT VIEW
It looks like this:
And here is the subview that the view above displays.. This is the one that needs a full "back button" to see the selected view.. If you clone these to make the other subviews you'll get the results (I hope:-))
import SwiftUI
struct SetAsideBalanceView: View {
#State var account_code: Int
#State var currentBudgetYear: Int
#State var totalBalances: Double = 0.00
// Array of SetAside Balance records for View List
var setAsideBalances: [SetAsideBalances.SetAsideBalancesRecord]
{
return SetAsideBalances.shared.selectSetAsideBalancesForAccount(withAccountCode: self.account_code)
}
var body: some View {
GeometryReader { gr in
let viewWidth = gr.size.width * 1
let columns = [
GridItem(.fixed(viewWidth * 0.60), alignment: .leading),
GridItem(.fixed(viewWidth * 0.30), alignment: .trailing)
]
VStack {
if setAsideBalances.count > 0 {
SetAsideBalanceHeader(accountName: setAsideBalances[0].account_name!, budgetYear: currentBudgetYear)
ScrollView {
ForEach (setAsideBalances, id: \.self) { setAsideRecord in
SetAsideBalancesRow(accountCode: account_code, setAsideBalanceCode: setAsideRecord.set_aside_code, description: setAsideRecord.description, set_aside_balance: setAsideRecord.set_aside_balance, currentBudgetYear: currentBudgetYear)
.frame(height: 45)
}
}
.frame(height: CGFloat((setAsideBalances.count + 1) * 45))
LazyVGrid(columns: columns, spacing: 0) {
Text("TOTAL BALANCES")
.padding(.leading, 20)
Text("\(NumberFormatter.formatAsCurrency(value: totalBalances))")
.foregroundColor((totalBalances < 0 ) ? Color("negative") : nil)
}
.frame(maxWidth: .infinity)
.frame(height: 55)
.border(Color.primary)
.foregroundColor(.black)
.background(Rectangle().fill(Color("lightBlue")))
.font(.title2)
Spacer()
}
} // END VSTACK
.font(.title2)
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(.top, 5)
.onAppear {
self.totalBalances = SetAsideBalances.shared.sumSetAsideBalancesForAccount(withAccountCode: self.account_code)
}
.font(.title2)
.navigationTitle("Managing Your Money - Set Aside")
.navigationBarTitleDisplayMode(.inline)
.ignoresSafeArea(edges: .bottom)
}
} // END BODY VIEW
} // END STRUCT VIEW
This is what the subview looks like:
I think I found it. We have to update the NavigationLink in the subview also to the new logic.
In SetAsideBalanceListView replace this:
ForEach (accounts, id: \.self) { accountRec in
let result = Budget.shared.getBudgetMonthAndYearForView(withAccountCode: accountRec.account_code)
NavigationLink(destination: SetAsideBalanceView(account_code: accountRec.account_code, currentBudgetYear: result.viewYear)){
HStack {
Image(systemName: "dollarsign.circle")
.foregroundColor(.white)
.background(Color("iconBlue"))
.cornerRadius(25)
.padding(.leading, 15)
Text(accountRec.account_name)
Spacer()
}
}
.foregroundColor(.primary)
Divider().background(Color.primary)
}// END FOR EACH
with this:
ForEach (accounts, id: \.self) { accountRec in
let result = Budget.shared.getBudgetMonthAndYearForView(withAccountCode: accountRec.account_code)
NavigationLink(value: accountRec) { // HERE
HStack {
Image(systemName: "dollarsign.circle")
.foregroundColor(.white)
.background(Color("iconBlue"))
.cornerRadius(25)
.padding(.leading, 15)
Text(accountRec.account_name)
Spacer()
}
}
.foregroundColor(.primary)
Divider().background(Color.primary)
}// END FOR EACH
// HERE
.navigationDestination(for: AccountRecord.self) { accountRec in
SetAsideBalanceView(account_code: accountRec.account_code, currentBudgetYear: result.viewYear)
}
You want to switch to NavigationSplitView(sidebar: content:). As the name implies it has two elements: 1 the sidebar, and 2 the content area.
I also would like to suggest to exchange the many if statements for the view selection with a switch statement on listSelection.
#State var listSelection: Int = 1
#Binding var isLoggedIn: Bool
var body: some View {
if DBManager.shared.checkDatabaseFileExists()
{
NavigationSplitView {
SideListView(listSelection: $listSelection)
} detail: {
switch listSelection {
case 1: AccountsListView()
case 2: SetAsideBalanceListView()
case 3: BankBalanceListView()
case 4: SetupMenuView()
case 5: ReportsListView()
default: Text("Select View")
}
}
} else {
Text("NO DATABASE FOUND!!!!")
.font(.system(size: 60))
} // END OUTTER IF/ELSE DATABASE FOUND
} // END BODY VIEW
extended version including detail links.
struct ContentView: View {
#State var listSelection: Int? = 1
// #Binding var isLoggedIn: Bool
var body: some View {
// if DBManager.shared.checkDatabaseFileExists()
// {
NavigationSplitView {
List(selection: $listSelection) {
Label("Accounts", systemImage: "dollarsign.circle").tag(1)
Label("Set Aside", systemImage: "folder").tag(2)
Label("Bank Bal.", systemImage: "line.2.horizontal.decrease.circle").tag(3)
Label("Setup", systemImage: "gear").tag(4)
Label("Reports", systemImage: "gear").tag(5)
}
} detail: {
switch listSelection {
case 1: SubView(title: "Accounts")
case 2: SubView(title: "Set Aside")
case 3: SubView(title: "Bank Balance")
case 4: SubView(title: "Setup")
case 5: SubView(title: "Reports")
default: Text("Select View")
}
}
// } else {
// Text("NO DATABASE FOUND!!!!")
// .font(.system(size: 60))
// } // END OUTTER IF/ELSE DATABASE FOUND
} // END BODY VIEW
} // END STRUCT VIEW
struct SubView: View {
let title: String
var body: some View {
NavigationStack {
List(0..<6, id: \.self) { nr in
NavigationLink("\(title) Subview \(nr)", value: nr) // select
}
.navigationDestination(for: Int.self) { nr in // define the destination (Int.self refers to type of selection)
DetailView(item: nr)
}
}
.navigationBarTitle(title)
}
}
struct DetailView: View {
let item: Int
var body: some View {
Text("This is the detail view for item \(item)")
}
}

SwiftUI - GeometryReader crashes SIGABRT

I'm trying to implement two different views depending on the device width. So the iPad Version of this view should be different to the iPhone version. To do this, I use the GeometryReader to check for the width. However, the app always crashes with "Thread 1: signal SIGABRT".
Each of the views on their own work perfectly fine.
If I start it in Splitscreen for iPad with a width less than 592, it works fine. I can change it to the big size afterwards without a crash. If start with a width greater than 592, it crashes.
Also if I only use the if statement without the else, it works.
Even the test on top crashes.
Here my code:
import SwiftUI
struct DetailView: View {
let food: FoodList
#State var showRightMenu = false
var body: some View {
GeometryReader { bounds in
ZStack (alignment: .topLeading) {
// Test
if bounds.size.width > 592 {
Text("Test")
} else {
Text("Test1")
Text("Test2")
}
// Actual code
// if bounds.size.width > 592 {
// HStack {
// FoodDetailPadViewLeft(food: self.food)
// .frame(width: bounds.size.width / 2)
//
// FoodDetailPadViewRight(food: self.food)
// }
// } else {
// ScrollView {
// FoodDetailViewImage(food: self.food)
// .animation(.none)
//
// FoodDetailViewNutris(food: self.food)
//
// Spacer()
// }
// }
HStack {
BackButton()
Spacer()
InfoButton(showRightMenu: self.$showRightMenu)
}
}
.background(Color("background"))
.edgesIgnoringSafeArea(.all)
.navigationBarTitle("")
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
}
Here is some reproducible code:
import SwiftUI
struct ContentView: View {
#State var foodlist: [FoodList] = Bundle.main.decode("ingredientsList.json")
var body: some View {
NavigationView {
List {
ForEach(foodlist) { food in
NavigationLink (destination: TestView(food: food)) {
Text(food.name)
}
}
}
}
}
}
struct TestView: View {
let food: FoodList
var body: some View {
GeometryReader { bounds in
ZStack (alignment: .topLeading) {
if bounds.size.width > 592 {
Text(self.food.name)
} else {
Text(self.food.name)
Text(self.food.category)
}
}
}
}
}
struct FoodList: Codable, Identifiable, Hashable {
let id: Int
let category: String
let name: String
}
extension Bundle {
func decode<T: Codable>(_ file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data (contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
let decoder = JSONDecoder()
guard let loaded = try? decoder.decode(T.self, from: data) else {
fatalError("Failed to decode \(file) from bundle.")
}
return loaded
}
}
and the Json-File:
[
{
"id": 1,
"category": "vegetables",
"name": "Tomato",
},
{
"id": 2,
"category": "vegetables",
"name": "Potato",
}
]
Any ideas?
This is that case when it is better to check explicitly for what you need:
var body: some View {
ZStack (alignment: .topLeading) {
if UIDevice.current.userInterfaceIdiom == .pad {
Text(self.food.name)
} else {
Text(self.food.name)
Text(self.food.category)
}
}
}
Note: the crash happens due to changed layout on the stack, and that happens because GeometryReader on same layout stack got different values (in first turn it is .zero, and second it is real), so different branches of your condition are activated on same layout stack and this makes SwiftUI rendering engine crazy. (You can submit feedback for this to Apple, but it is hardly to be resolved, because it is chicken-egg problem - Geometry reader always go two times).
Not shure if this is the right solution, but I used the opacity to hide the views when the width is greater than 592.
Seems to work for the moment.
var body: some View {
GeometryReader { bounds in
ZStack (alignment: .topLeading) {
Text(self.food.name)
.opacity(bounds.size.width > 592 ? 0 : 1)
VStack {
Text(self.food.name)
Text(self.food.category)
}
.opacity(bounds.size.width <= 592 ? 0 : 1)
}
}
}

How to make SwiftUI to forget internal state

I have a view like following
struct A: View {
var content: AnyView
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
// Common Elements
content
// More Common Elements
}
}
}
}
When I call this from another view like
A(nextInnerView())
two things happen. Firstly, as the size of the content element changes ScrollView animates the transition. Secondly, if you scroll down and then change the content the scrolling position does not reset.
Here is a demo of possible solution. Tested with Xcode 11.4 / iOS 13.4
The origin of this behaviour is in SwiftUI rendering optimisation, that tries to re-render only changed part, so approach is to identify view A (to mark it as completely changed) based on condition that originated in interview changes, alternatively it can be identified just by UUID().
struct TestInnerViewReplacement: View {
#State private var counter = 0
var body: some View {
VStack {
Button("Next") { self.counter += 1 }
Divider()
A(content: nextInnerView())
.id(counter) // << here !!
}
}
private func nextInnerView() -> AnyView {
AnyView(Group {
if counter % 2 == 0 {
Text("Text Demo")
} else {
Image(systemName: "star")
.resizable()
.frame(width: 100, height: 100)
}
})
}
}
struct A: View {
var content: AnyView
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(0..<5) { _ in // upper content demo
Rectangle().fill(Color.yellow)
.frame(height: 40)
.frame(maxWidth: .infinity)
.padding()
}
content
ForEach(0..<10) { _ in // lower content demo
Rectangle().fill(Color.blue)
.frame(height: 40)
.frame(maxWidth: .infinity)
.padding()
}
}
}
}
}

SwiftUI reduce spacing of rows in a list to null

I want to reduce the linespacing in a list to null.
My tries with reducing the padding did not work.
Setting ´.environment(.defaultMinListRowHeight, 0)´ helped a lot.
struct ContentView: View {
#State var data : [String] = ["first","second","3rd","4th","5th","6th"]
var body: some View {
VStack {
List {
ForEach(data, id: \.self)
{ item in
Text("\(item)")
.padding(0)
//.frame(height: 60)
.background(Color.yellow)
}
//.frame(height: 60)
.padding(0)
.background(Color.blue)
}
.environment(\.defaultMinListRowHeight, 0)
.onAppear { UITableView.appearance().separatorStyle = .none }
.onDisappear { UITableView.appearance().separatorStyle = .singleLine }
}
}
}
Changing the ´separatorStyle´ to ´.none´ only removed the Line but left the space.
Is there an extra ´hidden´ view for the Lists row or for the Separator between the rows?
How can this be controlled?
Would be using ScrollView instead of a List a good solution?
ScrollView(.horizontal, showsIndicators: true)
{
//List {
ForEach(data, id: \.self)
{ item in
HStack{
Text("\(item)")
Spacer()
}
Does it also work for a large dataset?
Well, actually no surprise - .separatorStyle = .none works correctly. I suppose you confused text background with cell background - they are changed by different modifiers. Please find below tested & worked code (Xcode 11.2 / iOS 13.2)
struct ContentView: View {
#State var data : [String] = ["first","second","3rd","4th","5th","6th"]
var body: some View {
VStack {
List {
ForEach(data, id: \.self)
{ item in
Text("\(item)")
.background(Color.yellow) // text background
.listRowBackground(Color.blue) // cell background
}
}
.onAppear { UITableView.appearance().separatorStyle = .none }
.onDisappear { UITableView.appearance().separatorStyle = .singleLine }
}
}
}
Update:
it's not possible to avoid the blue space between the yellow Texts?
Technically yes, it is possible, however for demo it is used hardcoded values and it is not difficult to fit some, while to calculate this dynamically might be challenging... anyway, here it is
it needs combination of stack for compression, content padding for resistance, and environment for limit:
List {
ForEach(data, id: \.self)
{ item in
HStack { // << A
Text("\(item)")
.padding(.vertical, 2) // << B
}
.listRowBackground(Color.blue)
.background(Color.yellow)
.frame(height: 12) // << C
}
}
.environment(\.defaultMinListRowHeight, 12) // << D
I do it the easy SwiftUI way:
struct ContentView: View {
init() {
UITableView.appearance().separatorStyle = .none
}
var body: some View {
List {
ForEach(0..<10){ item in
Color.green
}
.listRowInsets( EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) )
}
}
}
Reduce row spacing is really tricky, try
struct ContentView: View {
#State var data : [String] = ["first","second","3rd","4th","5th","6th"]
var body: some View {
VStack {
ScrollView {
ForEach(data, id: \.self) { item in
VStack(alignment: .leading, spacing: 0) {
Color.red.frame(height: 1)
Text("\(item)").font(.largeTitle)
.background(Color.yellow)
}.background(Color.green)
.padding(.leading, 10)
.padding(.bottom, -25)
.frame(maxWidth: .infinity)
}
}
}
}
}
It use ScrollView instead of List and negative padding.
I didn't find any solution based on List, we have to ask Apple to publish xxxxStyle protocols and underlying structures.
UPDATE
What about this negative padding value? For sure it depends on height of our row content and unfortunately on SwiftUI layout strategy. Lets try some more dynamic content! (we use zero padding to demostrate the problem to solve)
struct ContentView: View {
#State var data : [CGFloat] = [20, 30, 40, 25, 15]
var body: some View {
VStack {
ScrollView {
ForEach(data, id: \.self) { item in
VStack(alignment: .leading, spacing: 0) {
Color.red.frame(height: 1)
Text("\(item)").font(.system(size: item))
.background(Color.yellow)
}.background(Color.green)
.padding(.leading, 10)
//.padding(.bottom, -25)
.frame(maxWidth: .infinity)
}
}
}
}
}
Clearly the row spacing is not fixed value! We have to calculate it for every row separately.
Next code snippet demonstrate the basic idea. I used global dictionary (to store height and position of each row) and tried to avoid any high order functions and / or some advanced SwiftUI technic, so it is easy to see the strategy. The required paddings are calculated only once, in .onAppear closure
import SwiftUI
var _p:[Int:(CGFloat, CGFloat)] = [:]
struct ContentView: View {
#State var data : [CGFloat] = [20, 30, 40, 25, 15]
#State var space: [CGFloat] = []
func spc(item: CGFloat)->CGFloat {
if let d = data.firstIndex(of: item) {
return d < space.count ? space[d] : 0
} else {
return 0
}
}
var body: some View {
VStack {
ScrollView {
ForEach(data, id: \.self) { item in
VStack(alignment: .leading, spacing: 0) {
Color.red.frame(height: 1)
Text("\(item)")
.font(.system(size: item))
.background(Color.yellow)
}
.background(
GeometryReader { proxy->Color in
if let i = self.data.firstIndex(of: item) {
_p[i] = (proxy.size.height, proxy.frame(in: .global).minY)
}
return Color.green
}
)
.padding(.leading, 5)
.padding(.bottom, -self.spc(item: item))
.frame(maxWidth: .infinity)
}.onAppear {
var arr:[CGFloat] = []
_p.keys.sorted(by: <).forEach { (i) in
let diff = (_p[i + 1]?.1 ?? 0) - (_p[i]?.1 ?? 0) - (_p[i]?.0 ?? 0)
if diff < 0 {
arr.append(0)
} else {
arr.append(diff)
}
}
self.space = arr
}
}
}
}
}
Running the code I've got

SwiftUI: Prevent view being inside VStack to expand

I have such VStack with list inside it
VStack(alignment: .leading, spacing: 16) {
Text("Contacts")
.font(.custom("AvenirNext-DemiBold", size: 20))
.foregroundColor(Color("DarkTitle"))
.padding(8).layoutPriority(1)
List(self.contacts) { contact in
ContactOption(contact: contact)
.padding(.horizontal, 4)
} //.frame(height: 240)
}
The problem with this code is that List tries to expand content as much as it can here taking up entire screen in spite of having just 4 contacts.
I can set this height to fixed value using frame(height: 240)
I consider wether there is possibility to enforce List to wrap its content like Text() view does.
i.e. if there is 4 rows in List wrap content to display just this 4 rows, if there is 8 rows expand to this 8 rows. Then I could set some max height ex. 400 above which List could not expand anymore and then it will be scrollable.
ok, i tried a bit and i am not sure whether you can use it or not, but check this out: (just tap on add and remofe to see how the list gets bigger and smaller)
struct ContactOption : View {
var contact: String
var body: some View {
Text(contact)
}
}
struct ListView : View {
var contacts: [String]
var body : some View {
// List(self.contacts, id: \.self) { contact in
// ContactOption(contact: contact)
// .padding(.horizontal, 4)
// }
List {
ForEach (contacts, id: \.self) { contact in
Text (contact)
}
}
}
}
struct ContentView: View {
#State var contacts = ["Chris", "Joe", "Carla", "another"]
var body: some View {
VStack() {
HStack {
Button("Add") {
self.contacts.append("dust")
}
Button("Remove") {
self.contacts = self.contacts.dropLast()
}
}
Text("Contacts")
.foregroundColor(Color.blue)
.padding(8).layoutPriority(1)
Form {
ListView(contacts: contacts)
Section(footer: Text("hi")) {
Text("hi")
}
}
Divider()
Text("end list")
.foregroundColor(Color.orange)
}
}
}