I have a content in the safe area that reveals details on a button tap. I would like to allow the user to drag the handle to expand the content details as well, almost like the bottom sheet behaviour.
This is what I have so far:
struct ContentView: View {
#State private var isExpanded = false
var body: some View {
ScrollView {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.frame(maxWidth: .infinity)
.padding()
}
.safeAreaInset(edge: .bottom) {
VStack(spacing: 24) {
Capsule()
.frame(width: 50, height: 5)
.opacity(0.5)
HStack(spacing: 16) {
Image(systemName: "list.bullet")
Spacer()
VStack {
Text("Track 2")
}
.font(.caption)
Spacer()
Image(systemName: "backward.fill")
Image(systemName: "play.fill")
Image(systemName: "forward.fill")
Spacer()
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
if isExpanded {
HStack {
Image(systemName: "airplayaudio")
Image(systemName: "timer")
Spacer()
Text("Artist")
}
}
}
.padding([.bottom, .horizontal])
.padding(.top, 8)
.background(.ultraThinMaterial)
}
}
}
How can I add functionality to the Capsule to drag the handle to expand that animates in a way to stretch the content in an elastic way (not just swipe to expand, but to drag the content until the user let's go)?
You can use a DragGesture on your Capsule:
.gesture(
DragGesture()
.onEnded { value in
withAnimation(.spring()) {
let swipingUp = value.translation.height < 0
withAnimation {
isExpanded = swipingUp ? true: false
}
}
}
)
To allow for a stretch, you can use onChanged paired with a State variable:
struct ContentView: View {
#State private var isExpanded = false
#State private var stretch = CGFloat.zero
var body: some View {
ScrollView {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.frame(maxWidth: .infinity)
.padding()
}
.safeAreaInset(edge: .bottom) {
VStack(spacing: 24) {
Capsule()
.frame(width: 50, height: 5)
.opacity(0.5)
.gesture(
DragGesture()
.onChanged { newValue in
withAnimation(.spring()) {
stretch = abs(newValue.translation.height)
}
}.onEnded { value in
withAnimation(.spring()) {
isExpanded = value.translation.height < 0
stretch = .zero
}
}
)
HStack(spacing: 16) {
Image(systemName: "list.bullet")
Spacer()
VStack {
Text("Track 2")
}
.font(.caption)
Spacer()
Image(systemName: "backward.fill")
Image(systemName: "play.fill")
Image(systemName: "forward.fill")
Spacer()
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
if isExpanded {
HStack {
Image(systemName: "airplayaudio")
Image(systemName: "timer")
Spacer()
Text("Artist")
}
}
if stretch > 0 && !isExpanded {
Color.clear
.frame(height: stretch)
}
}.padding([.bottom, .horizontal])
.padding(.top, 8)
.background(.ultraThinMaterial)
}
}
}
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
Using XCode12, beta 4, I am trying to separate a header area and a ScrollView. I've tried both VStack and GeometryReader; however, when I click in the header area, the navigation link in the ScrollView item beneath it is triggered.
If I use a list, this undesired behavior is not observed. If I use the ScrollView in XCode 11.6 and build for iOS13, this undesired behavior is also not observed.
Did something change with ScrollViews in VStacks or GeometryReaders in iOS14? Why does the scrollview "slide" under the object in the VStack or GeometryReader?
struct ContentView: View {
var body: some View {
NavigationView{
//GeometryReader { geo in
VStack{
VStack{
Text("Blah")
}//.frame(height: geo.size.height*0.20)
VStack {
ScrollView {
ForEach(0..<10) { i in
NavigationLink(destination: Text("\(i)")) {
cardRow(i: i)
.frame(maxWidth: .infinity)
.foregroundColor(Color(UIColor.label))
.padding()
.background(RoundedRectangle(cornerRadius: 12))
.foregroundColor(Color(UIColor.secondarySystemFill))
.padding()
}
}
}
}//.frame(height: geo.size.height*0.90)
}
}.navigationViewStyle(StackNavigationViewStyle())
}
struct cardRow: View {
var i: Int
var body: some View {
HStack {
VStack {
Text("88")
}.hidden()
.overlay(
VStack{
Text("\(i)")
}
)
Divider()
.background(Color(UIColor.label))
Spacer()
}
}
}
}
Here is worked solution (tested with Xcode 12 / iOS 14)
VStack {
ScrollView {
ForEach(0..<10) { i in
NavigationLink(destination: Text("\(i)")) {
cardRow(i: i)
.frame(maxWidth: .infinity)
.foregroundColor(Color(UIColor.label))
.padding()
.background(RoundedRectangle(cornerRadius: 12))
.foregroundColor(Color(UIColor.secondarySystemFill))
.padding()
}
}
}.clipped().contentShape(Rectangle()) // << here !!
}
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)
}
}