Automatically adjustable view height based on text height in SwiftUI - ios

I am trying to create a view in SwiftUI where the background of the image on the left should scale vertically based on the height of the text on the right.
I tried a lot of different approaches, from GeometryReader to .layoutPriority(), but I haven't managed to get any of them to work.
Current state:
Desired state:
I know that I could imitate the functionality by hardcoding the .frame(100) for the example I posted, but as text on the right is dynamic, that wouldn't work.
This is full code for the view in the screenshot:
import SwiftUI
struct DynamicallyScalingView: View {
var body: some View {
HStack(spacing: 20) {
Image(systemName: "snow")
.font(.system(size: 32))
.padding(20)
.background(Color.red.opacity(0.4))
.cornerRadius(8)
VStack(alignment: .leading, spacing: 8) {
Text("My Title")
.foregroundColor(.white)
.font(.system(size: 13))
.padding(5)
.background(Color.black)
.cornerRadius(8)
Text("Dynamic text that can be of different leghts. Spanning from one to multiple lines. When it's multiple lines, the background on the left should scale vertically")
.font(.system(size: 13))
}
}
.padding(.horizontal)
}
}
struct DailyFactView_Previews: PreviewProvider {
static var previews: some View {
DynamicallyScalingView()
}
}

Here is a solution based on view preference key. Tested with Xcode 11.4 / iOS 13.4
struct DynamicallyScalingView: View {
#State private var labelHeight = CGFloat.zero // << here !!
var body: some View {
HStack(spacing: 20) {
Image(systemName: "snow")
.font(.system(size: 32))
.padding(20)
.frame(minHeight: labelHeight) // << here !!
.background(Color.red.opacity(0.4))
.cornerRadius(8)
VStack(alignment: .leading, spacing: 8) {
Text("My Title")
.foregroundColor(.white)
.font(.system(size: 13))
.padding(5)
.background(Color.black)
.cornerRadius(8)
Text("Dynamic text that can be of different leghts. Spanning from one to multiple lines. When it's multiple lines, the background on the left should scale vertically")
.font(.system(size: 13))
}
.background(GeometryReader { // << set right side height
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
}
.onPreferenceChange(ViewHeightKey.self) { // << read right side height
self.labelHeight = $0 // << here !!
}
.padding(.horizontal)
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}

This is the answer without workaround.
struct DynamicallyScalingView: View {
var body: some View {
HStack(spacing: 20) {
Image(systemName: "snow")
.frame(maxHeight: .infinity) // Add this
.font(.system(size: 32))
.padding(20)
.background(Color.red.opacity(0.4))
.cornerRadius(8)
VStack(alignment: .leading, spacing: 8) {
Text("My Title")
.foregroundColor(.white)
.font(.system(size: 13))
.padding(5)
.background(Color.black)
.cornerRadius(8)
Text("Dynamic text that can be of different leghts. Spanning from one to multiple lines. When it's multiple lines, the background on the left should scale vertically")
.font(.system(size: 13))
}
.frame(maxHeight: .infinity) // Add this
}
.padding(.horizontal)
.fixedSize(horizontal: false, vertical: true) // Add this
}
}

Related

Swift UI Scroll View: Set frame for drag indicator

I have following problem. I want to create a vertical ScrollView with many rows. At the bottom of the view I have an info bar which appears over the scroll view because I put all the items in a ZStack. Here is my code and what it produces:
struct ProblemView: View {
var body: some View {
ZStack {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ForEach(0..<100, id:\.self) {i in
HStack {
Text("Text \(i)")
.foregroundColor(.red)
Spacer()
Image(systemName: "plus")
.foregroundColor(.blue)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
Divider()
}
}
}
VStack {
Spacer()
HStack {
Text("Some Info here")
Image(systemName: "info.circle")
.foregroundColor(.blue)
}
.padding()
.frame(maxWidth: .infinity)
.ignoresSafeArea()
.background(.ultraThinMaterial)
}
}
}
}
struct ProblemView_Previews: PreviewProvider {
static var previews: some View {
ProblemView()
}
}
As you can see the drag indicator is hidden behind the info frame. Also the last item can't be seen because it is also behind the other frame. What
I want is that the drag indicator stops at this info frame. Why am I using a ZStack and not just a VStack? I want that this opacity effect behind the info frame, you get when you scroll.
A edit on my preview post has been added and therefore I cannot edit it... I am just gonna post the answer as an other one then.
This is the code that fixes your problem:
import SwiftUI
struct ProblemView: View {
var body: some View {
ScrollView {
VStack {
ForEach(0..<100, id:\.self) {i in
HStack {
Text("Text \(i)")
.foregroundColor(.red)
Spacer()
Image(systemName: "plus")
.foregroundColor(.blue)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
Divider()
}
}
.frame(maxWidth: .infinity)
}
.safeAreaInset(edge: .bottom) { // πŸ‘ˆπŸ»
VStack {
HStack {
Text("Some Info here")
Image(systemName: "info.circle")
.foregroundColor(.blue)
}
.padding()
.frame(maxWidth: .infinity)
.ignoresSafeArea()
.background(.ultraThinMaterial)
}
}
}
}
struct ProblemView_Previews: PreviewProvider {
static var previews: some View {
ProblemView()
}
}
We cannot control offset of indicator, but we can make all needed views visible by injecting last empty view with the same height (calculated dynamically) as info panel.
Here is possible approach. Tested with Xcode 13.2 / iOS 15.2
struct ProblemView: View {
#State private var viewHeight = CGFloat.zero
var body: some View {
ZStack {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ForEach(0..<100, id:\.self) {i in
HStack {
Text("Text \(i)")
.foregroundColor(.red)
Spacer()
Image(systemName: "plus")
.foregroundColor(.blue)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
Divider()
}
Color.clear
.frame(minHeight: viewHeight) // << here !!
}
}
VStack {
Spacer()
HStack {
Text("Some Info here")
Image(systemName: "info.circle")
.foregroundColor(.blue)
}
.padding()
.frame(maxWidth: .infinity)
.ignoresSafeArea()
.background(.ultraThinMaterial)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
}
}
.onPreferenceChange(ViewHeightKey.self) {
self.viewHeight = $0
}
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = value + nextValue()
}
}

KingFisher SwiftUI cancelOnDisappear method doesn't seem to prevent or cancel the call in a ScrollView

I have a swiftUI app. The app uses a scrollView LazyVGrid to present 3-photos wide by x rows. I am using the KingFisher Kingfisher package to download images. The performance of the app suffers as the cancelOnDisapper doesn't seem to be called. Thus, if the customer scrolls to the bottom of the 300+ photo list, the customer is required to wait until all 300+ photos are loaded to see the picture.
Whether I use .cancelOnDisappear(true) or not doesn't seem to make a difference.
It doesn't seem to matter where I put the .cancelOnDisappear(true) in the sequence of method calls.
I'm using the cancelOnDisappear method, but when I scroll down, every picture renders in the scrollView even if I scroll quickly. The onSuccess and onFailure methods don't seem to be called either as I am not seeing the debug statements from those calls.
Here is a snippit of the code:
var body: some View {
ZStack {
// specifics note set yet or calculating sale summary
if (!seriesList.isSpecificsListSet || isCalculatingSaleSummary) {
GeometryReader { geometry in
ProgressView("Loading...")
.scaleEffect(3, anchor: .center)
.progressViewStyle(CircularProgressViewStyle(tint: .red))
.frame(width: geometry.size.width,
height: geometry.size.height)
.zIndex(1)
}
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
).background(Color(.systemGray5))
.opacity(0.75)
.zIndex(1)
}
if isShowPhotos {
VStack {
ScrollView {
HStack(alignment: .center) {
VStack(alignment: .center) {
Text(kNewText)
.fontWeight(.semibold)
.frame(maxHeight: .infinity, alignment: .center)
Text(kLooseText)
.fontWeight(.semibold)
.frame(maxHeight: .infinity, alignment: .center)
}
VStack(alignment: .leading) {
HStack(spacing: 6) {
filterSwitch(specificsTypeTextEnum: .have, filterValue: $new_HaveShow)
filterSwitch(specificsTypeTextEnum: .want, filterValue: $new_WantShow)
filterSwitch(specificsTypeTextEnum: .sell, filterValue: $new_SellShow)
filterSwitch(specificsTypeTextEnum: .order, filterValue: $new_OrderShow)
Spacer()
}
HStack(spacing: 6) {
filterSwitch(specificsTypeTextEnum: .have, filterValue: $loose_HaveShow)
filterSwitch(specificsTypeTextEnum: .want, filterValue: $loose_WantShow)
filterSwitch(specificsTypeTextEnum: .sell, filterValue: $loose_SellShow)
Spacer()
}
}
} // hstack filter switches
SearchBar(searchText: $searchText, isSearching: $isSearching)
// beginning photos
LazyVGrid(columns: [
GridItem(.flexible(minimum: 100), spacing: 8, alignment: .top),
GridItem(.flexible(minimum: 100), spacing: 8, alignment: .top),
GridItem(.flexible(minimum: 100), spacing: 8)
], alignment: .leading, spacing: 9, content: {
switch showContextType {
case .series:
ForEach(showFigures
.filter({(
$0.seriesUniqueId == series.uniqueId && $0.searchString.lowercased().contains(searchText.lowercased()))
|| ($0.seriesUniqueId == series.uniqueId && searchText.isEmpty)}),
id: \.self)
{ figure in
FigurePhoto(figure: figure, needsRefresh: $needsRefresh)
}
case .whatIsNew:
ForEach(showFigures
.filter({
($0.addedDate > isNewAddedDate
&& $0.searchString.lowercased().contains(searchText.lowercased()))
|| ($0.addedDate > isNewAddedDate && searchText.isEmpty)})
.sorted(by: {$0.addedDate == $1.addedDate ? $0.figurePackageName < $1.figurePackageName : $0.addedDate > $1.addedDate}),
id: \.self)
{ figure in
FigurePhoto(figure: figure, needsRefresh: $needsRefresh)
}
case .allFigures:
ForEach(showFigures
.filter({
($0.searchString.lowercased().contains(searchText.lowercased()))
|| (searchText.isEmpty)})
.sorted(by: {$0.figurePackageName < $1.figurePackageName}),
id: \.self)
{ figure in
FigurePhoto(figure: figure, needsRefresh: $needsRefresh)
}
} // end showContent type switch
}) // end alignment & lazy grid
} // end list view
BannerVC().frame(width: 320, height: 50, alignment: .center)
} // end vstack
.navigationBarTitle(series.seriesName)
.navigationBarItems(
trailing: FigureListMenuItems(series: series,
showContextType: showContextType,
filterByPhase: $filterByPhase,
isShowPhotos: $isShowPhotos,
isCalculatingSaleSummary: $isCalculatingSaleSummary)
)
}
}
}
Code for the loaded photos
struct FigurePhoto: View {
#ObservedObject var figure: Figure
#Binding var needsRefresh: Bool
var body: some View {
NavigationLink(
destination: FigureDetailView(figure: figure, needsRefresh: $needsRefresh)) {
// name photo and specifics
VStack(alignment: .center, spacing: 4) {
// image and specifics
HStack(alignment: .top, spacing: 4) {
VStack(alignment: .center, spacing: 0) {
let image = figure.flickrPhotoString ?? ""
KFImage(URL(string: image))
.resizable()
.onSuccess { r in
#if DEBUG
print("success: \(r)")
#endif
}
.onFailure { error in
#if DEBUG
print("ERROR: Failure in KFImage: \(error.localizedDescription)")
#endif
}
.placeholder {
// Placeholder while downloading.
kMyToyBoxLogoImage
.resizable()
.opacity(0.3)
.scaledToFit()
.cornerRadius(22)
}
.cancelOnDisappear(true) // cancel if scrolled past
.scaledToFit()
.cornerRadius(22)
.overlay(
GeometryReader { geometry in
HStack {
Spacer()
VStack {
if figure.isSpecificsSet {
SpecificsImageOverlay(specificsType: .have,
newCount: figure.publishedSpecifics.new_haveCount,
looseCount: figure.publishedSpecifics.loose_haveCount)
SpecificsImageOverlay(specificsType: .want,
newCount: figure.publishedSpecifics.new_wantCount,
looseCount: figure.publishedSpecifics.loose_wantCount)
SpecificsImageOverlay(specificsType: .sell,
newCount: figure.publishedSpecifics.new_sellCount,
looseCount: figure.publishedSpecifics.loose_sellCount)
SpecificsImageOverlay(specificsType: .order,
newCount: figure.publishedSpecifics.new_orderCount,
looseCount: 0)
}
}
.frame(width: geometry.size.width/6)
}
}
, alignment: .bottom)
Text(figure.figurePackageName)
.font(.system(size: 10, weight: .semibold))
.lineLimit(1)
.padding(.top, 4)
// convert to strings to avoid commas
Text(figure.series.seriesName)
.font(.system(size: 9, weight: .regular))
.lineLimit(1)
Spacer()
} // end vstack
.onAppear() {
// primary front image use global unique id to avoid random matching numbers _small
figure.fetchFigureImageURL(withTags: figure.figureGlobalUniqueId, withText: "\(figure.figureGlobalUniqueId)\(kPrimaryFrontImageNameSuffix)\(kSmallSuffix)")
} // end vstack on appear
} // end hstack
} // end vstack
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.blue, lineWidth: 2)
)
} // end navigation link
} // end body
}
Based on the comment from #Asperi I moved the NavigationLink to an overlay. The performance is significantly improved and the .cancelOnDisappear(true) is functioning.
.overlay(
ZStack {
RoundedRectangle(cornerRadius: 16)
.stroke(Color.blue, lineWidth: 2)
NavigationLink(
destination: FigureDetailView(figure: figure, needsRefresh: $needsRefresh)) {
Rectangle()
.hidden()
}
}
)
Here is the full structure code for the PhotoView. I'm not sure if my implementation is ideal.
struct FigurePhoto: View {
#ObservedObject var figure: Figure
#Binding var needsRefresh: Bool
var body: some View {
// name photo and specifics
VStack(alignment: .center, spacing: 4) {
// image and specifics
HStack(alignment: .top, spacing: 4) {
VStack(alignment: .center, spacing: 0) {
let image = figure.flickrPhotoString ?? ""
KFImage(URL(string: image))
.resizable()
.onSuccess { r in
#if DEBUG
print("success: \(r)")
#endif
}
.onFailure { error in
#if DEBUG
print("ERROR: Failure in KFImage: \(error.localizedDescription)")
#endif
}
.placeholder {
// Placeholder while downloading.
kMyToyBoxLogoImage
.resizable()
.opacity(0.3)
.scaledToFit()
.cornerRadius(22)
}
.cancelOnDisappear(true) // cancel if scrolled past
.scaledToFit()
.cornerRadius(22)
.overlay(
GeometryReader { geometry in
HStack {
Spacer()
VStack {
if figure.isSpecificsSet {
SpecificsImageOverlay(specificsType: .have,
newCount: figure.publishedSpecifics.new_haveCount,
looseCount: figure.publishedSpecifics.loose_haveCount)
SpecificsImageOverlay(specificsType: .want,
newCount: figure.publishedSpecifics.new_wantCount,
looseCount: figure.publishedSpecifics.loose_wantCount)
SpecificsImageOverlay(specificsType: .sell,
newCount: figure.publishedSpecifics.new_sellCount,
looseCount: figure.publishedSpecifics.loose_sellCount)
SpecificsImageOverlay(specificsType: .order,
newCount: figure.publishedSpecifics.new_orderCount,
looseCount: 0)
}
}
.frame(width: geometry.size.width/6)
}
}
, alignment: .bottom)
Text(figure.figurePackageName)
.font(.system(size: 10, weight: .semibold))
.lineLimit(1)
.padding(.top, 4)
// convert to strings to avoid commas
Text(figure.series.seriesName)
.font(.system(size: 9, weight: .regular))
.lineLimit(1)
Spacer()
} // end vstack
.onAppear() {
// primary front image use global unique id to avoid random matching numbers _small
figure.fetchFigureImageURL(withTags: figure.figureGlobalUniqueId, withText: "\(figure.figureGlobalUniqueId)\(kPrimaryFrontImageNameSuffix)\(kSmallSuffix)")
} // end vstack on appear
} // end hstack
} // end vstack
.overlay(
ZStack {
RoundedRectangle(cornerRadius: 16)
.stroke(Color.blue, lineWidth: 2)
NavigationLink(
destination: FigureDetailView(figure: figure, needsRefresh: $needsRefresh)) {
Rectangle()
.hidden()
}
}
)
} // end body
}

Swift UI Creating List Bubble Effect

I am trying to recreate the following list effect in SwiftUI,
List(){
TaskRowComponent(coreRouter: CoreRouter())
}
.listRowBackground(ColorScheme().field())
.cornerRadius(10)
.padding()
import SwiftUI
struct TaskRowComponent: View{
#ObservedObject var coreRouter: CoreRouter;
var body: some View {
VStack{
Text("This is a row!")
}
.listRowBackground(Color.green)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 80, maxHeight: 80, alignment: .leading)
}
}
I am trying to piece together how to create this effect where the two rows are stacked on top of each other, I do not want them to be packed how list view currently does it. I tried to add padding but it doesn't seem to work, any help would be much appreciated.
Something like this should get you going:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
VStack {
List {
ForEach([1, 2], id: \.self) { item in
ZStack {
Rectangle()
.blendMode(.overlay)
.frame(height: 100)
.background(LinearGradient(gradient: Gradient(colors: [Color.blue, Color.purple]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(9)
HStack {
VStack {
Text("Bikram Sinkemana")
.font(.system(size: 15))
.foregroundColor(.white)
Text("Kiran Regmi")
.font(.system(size: 15))
.foregroundColor(.white)
}
Text("3:1")
.font(.system(size: 30))
.foregroundColor(.white)
VStack {
Text("Bikram Sinkemana")
.font(.system(size: 15))
.foregroundColor(.white)
Text("Sagun Karanjit")
.font(.system(size: 15))
.foregroundColor(.white)
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

ScrollView content not filling in SwiftUI

I have a scrollview which content is a VStack containing a ForEach loop to create some Rows instead of a list. A List has some downsides like the dividers.
My Issue is that the Row is not filling the scrollview. I think the scrollview width is not filling the screen.
NavigationView {
Toggle(isOn: $onlineStatus) {
Text("Online Only")
}.padding([.leading, .trailing], 15)
ScrollView {
VStack(alignment: .trailing) {
ForEach(onlineStatus ? self.notes.filter { $0.dot == .green } : self.notes) { note in
NavigationButton(destination: Text("LOL")) {
CardRow(note: note)
.foregroundColor(.primary)
.cornerRadius(8)
.shadow(color: .gray, radius: 3, x: 0, y: -0.01)
}.padding([.leading, .trailing, .top], 5)
}.animation(self.onlineStatus ? .fluidSpring() : nil)
}
}.padding([.leading, .trailing])
.navigationBarTitle(Text("Your documents"))
}
This is giving me this result:
That's my CardRow:
struct CardRow: View {
var note: Note
var body: some View {
HStack {
Image(uiImage: UIImage(named: "writing.png")!)
.padding(.leading, 10)
VStack(alignment: .leading) {
Group {
Text(note.message)
.font(.headline)
Text(note.date)
.font(.subheadline)
}
.foregroundColor(.black)
}
Spacer()
VStack(alignment: .trailing) {
Circle().foregroundColor(note.dot)
.frame(width: 7, height: 7)
.shadow(radius: 1)
.padding(.trailing, 5)
Spacer()
}.padding(.top, 5)
}
.frame(height: 60)
.background(Color(red: 237/255, green: 239/255, blue: 241/255))
}
}
Use .frame(minWidth: 0, maxWidth: .infinity) on the RowView or inside of it
Best solution I found is to use GeometryReader to fill the ScrollView's contents to the outer view's width. And be sure to use a Spacer() in each row's HStack. This handles safe areas and rotation well.
struct Card : View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("Header")
.font(.headline)
Text("Description")
.font(.body)
}
Spacer()
}
.padding()
.background(Color.white)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 0)
}
}
struct Home : View {
var body: some View {
GeometryReader { geometry in
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Card()
Card()
Card()
}
.padding()
.frame(width: geometry.size.width)
}
.navigationBarTitle(Text("Home"))
}
}
}
}
See resulting screenshot:
Try setting the frame width of the RowView to UIScreen.main.bounds.width:
.frame(width: UIScreen.main.bounds.width)

Make a VStack fill the width of the screen in SwiftUI

Given this code:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.lineLimit(nil)
.font(.body)
Spacer()
}
.background(Color.red)
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
It results in this interface:
How can I make the VStack fill the width of the screen even if the labels/text components don't need the full width?
A trick I've found is to insert an empty HStack in the structure like so:
VStack(alignment: .leading) {
HStack {
Spacer()
}
Text("Title")
.font(.title)
Text("Content")
.lineLimit(nil)
.font(.body)
Spacer()
}
Which yields the desired design:
Is there a better way?
Try using the .frame modifier with the following options:
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello World")
.font(.title)
Text("Another")
.font(.body)
Spacer()
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.background(Color.red)
}
}
This is described as being a flexible frame (see the documentation), which will stretch to fill the whole screen, and when it has extra space it will center its contents inside of it.
With Swift 5.2 and iOS 13.4, according to your needs, you can use one of the following examples to align your VStack with top leading constraints and a full size frame.
Note that the code snippets below all result in the same display, but do not guarantee the effective frame of the VStack nor the number of View elements that might appear while debugging the view hierarchy.
1. Using frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) method
The simplest approach is to set the frame of your VStack with maximum width and height and also pass the required alignment in frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:):
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.font(.body)
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.background(Color.red)
}
}
2. Using Spacers to force alignment
You can embed your VStack inside a full size HStack and use trailing and bottom Spacers to force your VStack top leading alignment:
struct ContentView: View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.font(.body)
Spacer() // VStack bottom spacer
}
Spacer() // HStack trailing spacer
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity
)
.background(Color.red)
}
}
3. Using a ZStack and a full size background View
This example shows how to embed your VStack inside a ZStack that has a top leading alignment. Note how the Color view is used to set maximum width and height:
struct ContentView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Color.red
.frame(maxWidth: .infinity, maxHeight: .infinity)
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.font(.body)
}
}
}
}
4. Using GeometryReader
GeometryReader has the following declaration:
A container view that defines its content as a function of its own size and coordinate space. [...] This view returns a flexible preferred size to its parent layout.
The code snippet below shows how to use GeometryReader to align your VStack with top leading constraints and a full size frame:
struct ContentView : View {
var body: some View {
GeometryReader { geometryProxy in
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.font(.body)
}
.frame(
width: geometryProxy.size.width,
height: geometryProxy.size.height,
alignment: .topLeading
)
}
.background(Color.red)
}
}
5. Using overlay(_:alignment:) method
If you want to align your VStack with top leading constraints on top of an existing full size View, you can use overlay(_:alignment:) method:
struct ContentView: View {
var body: some View {
Color.red
.frame(
maxWidth: .infinity,
maxHeight: .infinity
)
.overlay(
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.font(.body)
},
alignment: .topLeading
)
}
}
Display:
An alternative stacking arrangement which works and is perhaps a bit more intuitive is the following:
struct ContentView: View {
var body: some View {
HStack() {
VStack(alignment: .leading) {
Text("Hello World")
.font(.title)
Text("Another")
.font(.body)
Spacer()
}
Spacer()
}.background(Color.red)
}
}
The content can also easily be re-positioned by removing the Spacer()'s if necessary.
There is a better way!
To make the VStack fill the width of it's parent you can use a GeometryReader and set the frame. (.relativeWidth(1.0) should work but apparently doesn't right now)
struct ContentView : View {
var body: some View {
GeometryReader { geometry in
VStack {
Text("test")
}
.frame(width: geometry.size.width,
height: nil,
alignment: .topLeading)
}
}
}
To make the VStack the width of the actual screen you can use UIScreen.main.bounds.width when setting the frame instead of using a GeometryReader, but I imagine you likely wanted the width of the parent view.
Also, this way has the added benefit of not adding spacing in your VStack which might happen (if you have spacing) if you added an HStack with a Spacer() as it's content to the VStack.
UPDATE - THERE IS NOT A BETTER WAY!
After checking out the accepted answer, I realized that the accepted answer doesn't actually work! It appears to work at first glance, but if you update the VStack to have a green background you'll notice the VStack is still the same width.
struct ContentView : View {
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Hello World")
.font(.title)
Text("Another")
.font(.body)
Spacer()
}
.background(Color.green)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
.background(Color.red)
}
}
}
This is because .frame(...) is actually adding another view to the view hierarchy and that view ends up filling the screen. However, the VStack still does not.
This issue also seems to be the same in my answer as well and can be checked using the same approach as above (putting different background colors before and after the .frame(...). The only way that appears to actually widen the VStack is to use spacers:
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
HStack{
Text("Title")
.font(.title)
Spacer()
}
Text("Content")
.lineLimit(nil)
.font(.body)
Spacer()
}
.background(Color.green)
}
}
The simplest way I manage to solve the issue was is by using a ZStack + .edgesIgnoringSafeArea(.all)
struct TestView : View {
var body: some View {
ZStack() {
Color.yellow.edgesIgnoringSafeArea(.all)
VStack {
Text("Hello World")
}
}
}
}
Way Number 1 -> Using MaxWidth & MaxHeight
import SwiftUI
struct SomeView: View {
var body: some View {
VStack {
Text("Hello, World!")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
Way Number 2 -> Using Main Screen Bounds
import SwiftUI
struct SomeView: View {
var body: some View {
VStack {
Text("Hello, World!")
}
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height)
.background(.red)
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
Way Number 3 -> Using Geometry Reader
import SwiftUI
struct SomeView: View {
var body: some View {
GeometryReader { geometryReader in
VStack {
Text("Hello, World!")
}
.frame(maxWidth: geometryReader.size.width, maxHeight: geometryReader.size.height)
.background(.red)
}
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
Way Number 4 -> Using Spacers
import SwiftUI
struct SomeView: View {
var body: some View {
VStack {
Text("Hello, World!")
HStack{
Spacer()
}
Spacer()
}
.background(.red)
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
EDIT: answer updated with simple (better) approach using .frame
Just use frame modifiers!
struct Expand: View {
var body: some View {
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.lineLimit(nil)
.font(.body)
}
.frame(maxWidth:.infinity,maxHeight:.infinity,alignment:.topLeading)
.background(Color.red)
}
}
note - you don't even need the spacer in the VStack!
note2 - if you don't want the white at top & bottom, then in the background use:
Color.red.edgesIgnoringSafeArea(.all)
use this
.edgesIgnoringSafeArea(.all)
A good solution and without "contraptions" is the forgotten ZStack
ZStack(alignment: .top){
Color.red
VStack{
Text("Hello World").font(.title)
Text("Another").font(.body)
}
}
Result:
You can do it by using GeometryReader
GeometryReader
Code:
struct ContentView : View {
var body: some View {
GeometryReader { geometry in
VStack {
Text("Turtle Rock").frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading).background(Color.red)
}
}
}
}
Your output like:
One more alternative is to place one of the subviews inside of an HStack and place a Spacer() after it:
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Title")
.font(.title)
.background(Color.yellow)
Spacer()
}
Text("Content")
.lineLimit(nil)
.font(.body)
.background(Color.blue)
Spacer()
}
.background(Color.red)
}
}
resulting in :
This is a useful bit of code:
extension View {
func expandable () -> some View {
ZStack {
Color.clear
self
}
}
}
Compare the results with and without the .expandable() modifier:
Text("hello")
.background(Color.blue)
-
Text("hello")
.expandable()
.background(Color.blue)
This is what worked for me (ScrollView (optional) so more content can be added if needed, plus centered content):
import SwiftUI
struct SomeView: View {
var body: some View {
GeometryReader { geometry in
ScrollView(Axis.Set.horizontal) {
HStack(alignment: .center) {
ForEach(0..<8) { _ in
Text("πŸ₯³")
}
}.frame(width: geometry.size.width, height: 50)
}
}
}
}
// MARK: - Preview
#if DEBUG
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
#endif
Result
I know this will not work for everyone, but I thought it interesting that just adding a Divider solves for this.
struct DividerTest: View {
var body: some View {
VStack(alignment: .leading) {
Text("Foo")
Text("Bar")
Divider()
}.background(Color.red)
}
}
Login Page design using SwiftUI
import SwiftUI
struct ContentView: View {
#State var email: String = "william#gmail.com"
#State var password: String = ""
#State static var labelTitle: String = ""
var body: some View {
VStack(alignment: .center){
//Label
Text("Login").font(.largeTitle).foregroundColor(.yellow).bold()
//TextField
TextField("Email", text: $email)
.textContentType(.emailAddress)
.foregroundColor(.blue)
.frame(minHeight: 40)
.background(RoundedRectangle(cornerRadius: 10).foregroundColor(Color.green))
TextField("Password", text: $password) //Placeholder
.textContentType(.newPassword)
.frame(minHeight: 40)
.foregroundColor(.blue) // Text color
.background(RoundedRectangle(cornerRadius: 10).foregroundColor(Color.green))
//Button
Button(action: {
}) {
HStack {
Image(uiImage: UIImage(named: "Login")!)
.renderingMode(.original)
.font(.title)
.foregroundColor(.blue)
Text("Login")
.font(.title)
.foregroundColor(.white)
}
.font(.headline)
.frame(minWidth: 0, maxWidth: .infinity)
.background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.padding(.horizontal, 20)
.frame(width: 200, height: 50, alignment: .center)
}
Spacer()
}.padding(10)
.frame(minWidth: 0, idealWidth: .infinity, maxWidth: .infinity, minHeight: 0, idealHeight: .infinity, maxHeight: .infinity, alignment: .top)
.background(Color.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
⚠️ Important Note!
πŸ›‘ All other solutions are just adding a frame around the content!
βœ… but this solution changes the actual frame!
Simple and correct extension
You can use this modifier
.flexible(width: true, height: false)
Demo
πŸ’‘Note how contents are aligned exactly as you assign in the original stack
The code behind this ( FlexibleViewModifier.swift )
extension View {
func flexible(width: Bool, height: Bool) -> some View {
self.modifier(MatchingParentModifier(width: width, height: height))
}
}
struct MatchingParentModifier: ViewModifier {
#State private var intrinsicSize: CGSize = UIScreen.main.bounds.size
private let intrinsicWidth: Bool
private let intrinsicHeight: Bool
init(width: Bool, height: Bool) {
intrinsicWidth = !width
intrinsicHeight = !height
}
func body(content: Content) -> some View {
GeometryReader { _ in
content.modifier(intrinsicSizeModifier(intrinsicSize: $intrinsicSize))
}
.frame(
maxWidth: intrinsicWidth ? intrinsicSize.width : nil,
maxHeight: intrinsicHeight ? intrinsicSize.height : nil
)
}
}
struct intrinsicSizeModifier: ViewModifier {
#Binding var intrinsicSize: CGSize
func body(content: Content) -> some View {
content.readIntrinsicContentSize(to: $intrinsicSize)
}
}
struct IntrinsicContentSizePreferenceKey: PreferenceKey {
static let defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
extension View {
func readIntrinsicContentSize(to size: Binding<CGSize>) -> some View {
background(
GeometryReader {
Color.clear.preference(
key: IntrinsicContentSizePreferenceKey.self,
value: $0.size
)
}
)
.onPreferenceChange(IntrinsicContentSizePreferenceKey.self) {
size.wrappedValue = $0
}
}
}
Here another way which would save time in your projects:
Much less code and reusable in compare to other answers which they are not reusable!
extension View {
var maxedOut: some View {
return Color.clear
.overlay(self, alignment: .center)
}
func maxedOut(color: Color = Color.clear, alignment: Alignment = Alignment.center) -> some View {
return color
.overlay(self, alignment: alignment)
}
}
use case:
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.maxedOut
.background(Color.blue)
Text("Hello, World!")
.maxedOut(color: Color.red)
}
}
Just add Color.clear to the bottom of the VStack, simple as that :)
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Title")
Color.clear
}
.background(Color.red)
}
}

Resources