Custom collapsible view - ios

I'm trying to create a custom collapsible view. The code works but in Collapsible<Content: View> the VStack has strange behavior: the elements overlap when the component is closed.
To note this, try disable clipped() as shown in the image.
Is it a bug or something so stupid that I am not noticing?
Thanks in advance
FIXED CODE:
struct Collapsible<Content: View>: View {
var label: String
var content: () -> Content
init(label: String, #ViewBuilder _ content: #escaping () -> Content) {
self.label = label
self.content = content
}
#State private var collapsed: Bool = true
var body: some View {
VStack(spacing: 0) {
Button(action: {
withAnimation(.easeInOut) {
self.collapsed.toggle()
}
}, label: {
HStack {
Text(label)
Spacer(minLength: 0)
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding()
.background(Color.white.opacity(0.1))
}
)
.buttonStyle(PlainButtonStyle())
self.content()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: collapsed ? 0 : .none, alignment: .top) // <- added `alignment` here
.clipped() // Comment to see the overlap
.animation(.easeOut)
.transition(.slide)
}
}
}
struct CollapsibleDemoView: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Collapsible(label: "Collapsible") {
Text("Content")
.padding()
.background(Color.red)
}
Spacer(minLength: 0)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

The .frame modifier has a parameter alignment which defaults to center which results in the behaviour you're seeing: the layout bounds are set to zero height but the content is rendered vertically centered beyond the bounds (if not clipped). You can fix this by adding the alignment:
.frame(maxHeight: 0, alignment: .top)

Related

SwiftUI TextEditor Divider doesn't change Y position based on text-line count?

I am trying to make a SwiftUI TextEditor with a Divider that adapts its position to stay under the bottom-most line of text inside of a edit-bio section of the app.
Note: I have a frame on my TextEditor so that it doesn't take up the whole-screen
Right now the Divider is static and stays in one place. Is there a built-in way to make the divider stay under the bottom most line of text?
I would think the Spacer would have given me this behavior?
Thank you!
struct EditBio: View {
#ObservedObject var editProfileVM: EditProfileViewModel
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM.bio)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
Divider().frame(height: 1).background(.white)
Spacer()
}
}
}
It is doing exactly what you told it to do. But a background color on your TextEditor. You will see that it has a height of 200 + a spacing of 10 from the VStack.
I changed your code to make it obvious:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.background(Color.gray)
Divider().frame(height: 1).background(.red)
Spacer()
}
}
}
to produce this:
You can see the TextEditor naturally wants to be taller than 200, but that is limiting it. Therefore, the Spacer() is not going to cause the TextEditor to be any smaller.
The other problem that setting a fixed frame causes will be that your text will end up off screen at some point. I am presuming what you really want is a self sizing TextEditor that is no larger than it's contents.
That can be simply done with the following code:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
SelfSizingTextEditor(text: $editProfileVM)
// Frame removed for the image below.
// .frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.foregroundColor(.white)
// made the .top padding to be .vertical
.padding(.vertical, 70)
.padding([.leading, .trailing], 50)
.background(Color.gray)
Divider().frame(height: 5).background(.red)
Spacer()
}
}
}
struct SelfSizingTextEditor: View {
#Binding var text: String
#State var textEditorSize = CGSize.zero
var body: some View {
ZStack {
Text(text)
.foregroundColor(.clear)
.copySize(to: $textEditorSize)
TextEditor(text: $text)
.frame(height: textEditorSize.height)
}
}
}
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
func copySize(to binding: Binding<CGSize>) -> some View {
self.readSize { size in
binding.wrappedValue = size
}
}
}
producing this view:

How to show views on top of the Navigation Bar from within a child-linked view

I'm trying to have a sliding side menu cover the screen as well as the navigation bar. I was following this tutorial to achieve it, and it works well when you have the NavigationView declared within the actual view so you can embed it outside your NavigationView as a sibling in a ZStack.
However, anytime I try to pull this view in from a NavigationLink, the views on the sliding menu are "visible" while also blocked by the navigation bar and its views. Any ideas on how to fix this?
Side Menu code:
struct SlidingSideMenu<Content: View>: View {
let width: CGFloat
let isOpen: Bool
let menuClose: () -> Void
let content: Content
init(width: CGFloat, isOpen: Bool, menuClose: #escaping () -> Void, #ViewBuilder content: () -> Content) {
self.width = width
self.isOpen = isOpen
self.menuClose = menuClose
self.content = content()
}
var body: some View {
ZStack {
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.3))
.opacity(self.isOpen ? 1.0 : 0.0)
.animation(Animation.easeIn.delay(0.25))
.onTapGesture {
self.menuClose()
}
.edgesIgnoringSafeArea(.all)
HStack {
MenuContent(menuClose: menuClose) {
self.content
}
.frame(width: self.width)
.background(Color.white)
.offset(x: self.isOpen ? 0 : -self.width)
.animation(.default)
.edgesIgnoringSafeArea(.top)
Spacer()
}
}
}
}
Working:
struct MainDetailsView: View {
#State var menuOpen: Bool = false
var body: some View {
ZStack {
NavigationView {
ScrollView {
Text("Page Content")
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationBarTitle("Nav title")
.navigationBarItems(leading: Button(action: { self.openMenu() }) {
Image(systemName: "magnifyingglass.circle.fill")
.font(.system(size: 25))
.foregroundColor(Color.secondary)
}.frame(maxWidth: .infinity, alignment: .leading))
}
SlidingSideMenu(width: 270,
isOpen: self.menuOpen,
menuClose: self.openMenu){
VStack{
Text("Side Menu Content")
Spacer()
}
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.edgesIgnoringSafeArea(.all)
}
}
func openMenu() {
self.menuOpen.toggle()
}
}
struct MainDetailsView_Previews: PreviewProvider {
static var previews: some View {
MainDetailsView()
}
}
Working GIF
Not Working:
struct ParentNavView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: MainDetailsView()) {
Text("N A V I G A T E")
}
}
}
}
struct MainDetailsView: View {
#State var menuOpen: Bool = false
var body: some View {
ZStack {
// NavView removed
ScrollView {
Text("Page Content")
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationBarTitle("Nav title")
.navigationBarItems(leading: Button(action: { self.openMenu() }) {
Image(systemName: "magnifyingglass.circle.fill")
.font(.system(size: 25))
.foregroundColor(Color.secondary)
}.frame(maxWidth: .infinity, alignment: .leading))
SlidingSideMenu(width: 270,
isOpen: self.menuOpen,
menuClose: self.openMenu){
VStack{
Text("Side Menu Content")
Spacer()
}
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.edgesIgnoringSafeArea(.all)
}
}
func openMenu() {
self.menuOpen.toggle()
}
}
struct MainDetailsView_Previews: PreviewProvider {
static var previews: some View {
ParentNavView()
}
}
Not working GIF

SwiftUI: PreferenceKey is not called when parent view is embedded in NavigationView

DynamicScalingView is a child view with two buttons designed to have equal width & height using preferencekey
Issue: When DynamicScalingView is embedded in NavigationView it not longer adapts to intrinsic and increases its frame size. Current implementation without Navigation works fine but would like to understand how to fix this issue when embedded in NavigationView
Uncomment NavigationView in sample view to reproduce the issue
DynamicScalingView should adapt to Dynamic font size and increase its frame size maintaining equal width & height constraint between the buttons.
XCode 12.2 and iOS 14.2
struct SampleView: View {
var body: some View {
GeometryReader { gr in
//NavigationView {
ScrollView {
VStack {
// fills whatever space is left
Spacer()
.foregroundColor(.clear)
// view should fit with intrinsic content size
DynamicScalingView()
.padding(.horizontal, 20)
.border(Color.blue, width: 1)
}
.padding(.bottom, 20)
.border(Color.red, width: 1)
.frame(minHeight: gr.size.height)
.navigationBarHidden(true)
}
// }
}
}
struct DynamicScalingView: View {
#State private var labelHeight = CGFloat.zero
var body: some View {
HStack {
Button(action: {}, label: {
Text("Some Text Some Text Some Text")
.padding(.horizontal, 2)
})
.foregroundColor(Color.white)
.padding(.vertical)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(minHeight: labelHeight)
.background(Color.blue)
.cornerRadius(8)
.fixedSize(horizontal: false, vertical: true)
.background(GeometryReader {
Color.clear
.preference(
key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height
)
})
Button(action: {}, label: {
Text("Some Text")
.padding(.horizontal, 2)
})
.foregroundColor(Color.white)
.padding(.vertical)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(minHeight: labelHeight)
.background(Color.blue)
.cornerRadius(8)
.fixedSize(horizontal: false, vertical: true)
.background(GeometryReader {
Color.clear
.preference(
key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height
)
})
}
.onPreferenceChange(ViewHeightKey.self) {
self.labelHeight = $0
}
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = max(value, nextValue())
}
}
}

SwiftUI HStack elements with equal height

I want both the buttons to have equal height similar to Equal Height constraint in UIKit.
Don't want to specify the frame, let SwiftUI handle it but the elements in HStack should be of the same height.
Buttons should have equal width and height and adapt to longer text and increases their frame size
Both the buttons should display their complete text (Font to scale / Fit shouldn't be used)
Sample Code
struct SampleView: View {
var body: some View {
GeometryReader { gr in
VStack {
ScrollView {
VStack {
// Fills whatever space is left
Rectangle()
.foregroundColor(.clear)
Image(systemName: "applelogo")
.resizable()
.frame(width: gr.size.width * 0.5, height: gr.size.height * 0.3, alignment: .center)
//.border(Color.blue)
.padding(.bottom, gr.size.height * 0.06)
Text("SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME")
.fontWeight(.regular)
.foregroundColor(.green)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
.layoutPriority(1)
// Fills 15 %
Rectangle()
.frame(height: gr.size.height * 0.12)
.foregroundColor(.clear)
DynamicallyScalingView()
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
// Makes the content stretch to fill the whole scroll view, but won't be limited (it can grow beyond if needed)
.frame(minHeight: gr.size.height)
}
}
}
}
}
struct DynamicallyScalingView: View {
#State private var labelHeight = CGFloat.zero // << here !!
var body: some View {
HStack {
Button(action: {
}, label: {
Text("Button 1")
})
.foregroundColor(Color.white)
.padding(.vertical)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(minHeight: labelHeight)
.background(Color.blue)
.cornerRadius(8)
Button(action: {
}, label: {
Text("Larger Button 2 Text Text2")
})
.foregroundColor(Color.white)
.padding(.vertical)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(8)
.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()
}
}
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
SampleView().previewDevice("iPhone SE (2nd generation)")
}
}
You can set the max value in the ViewHeightKey preference key:
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = max(value, nextValue()) // set the `max` value (from both buttons)
}
}
and then read view height from both buttons and force vertical fixedSize:
struct DynamicallyScalingView: View {
#State private var labelHeight = CGFloat.zero
var body: some View {
HStack {
Button(action: {}, label: {
Text("SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME TEXT SOME")
})
.foregroundColor(Color.white)
.padding(.vertical)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(minHeight: labelHeight) // min height for both buttons
.background(Color.blue)
.cornerRadius(8)
.fixedSize(horizontal: false, vertical: true) // expand vertically
.background(GeometryReader { // apply to both buttons
Color.clear
.preference(
key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height
)
})
Button(action: {}, label: {
Text("jahlsd")
})
.foregroundColor(Color.white)
.padding(.vertical)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(minHeight: labelHeight)
.background(Color.blue)
.cornerRadius(8)
.fixedSize(horizontal: false, vertical: true)
.background(GeometryReader {
Color.clear
.preference(
key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height
)
})
}
.onPreferenceChange(ViewHeightKey.self) {
self.labelHeight = $0
}
.padding(.horizontal)
}
}
Note: as the buttons are similar now, the next step would be to extract them as another component to avoid duplication.

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