How to create sticky bottom in SwiftUI? - ios

I am trying to create sticky footer in swiftUI where other part of screen is scrollable but in footer there is one view with buttons and other element which should be fixed.
Thank You for help.

If I understand correctly, what you want to do is stack vertically (VStack)
a Scrollview
another VStack (with the Toggle and the Button), aligned at the bottom :
VStack {
ScrollView {...} // 1
VStack { // 2
Toggle(...)
Button(...)
}
.frame(alignment: .bottom)
}
To take your example :
struct SwiftUIView: View {
#State private var checked: Bool = false
let text = String(repeating: "blabla ", count: 20)
var body: some View {
VStack {
ScrollView {
ForEach((1...100), id: \.self) {_ in
Text(text)
}
}
VStack {
Toggle(isOn: $checked, label: {
Text("I have read...")
})
Button("Enter") {
// action
}
.frame(maxWidth: .infinity)
.padding(.vertical)
.background(Color.red)
}
.padding()
.border(Color.black)
.frame(alignment: .bottom)
}
}
}

Related

Animate only one view inside VStack

My main view is as simple as follows:
VStack(alignment: .center, spacing: .spacing4) {
ExpandableView() // some params for title etc, not relevant
ExpandableView()
}
.padding(.horizontal, 10)
.padding(.vertical, 18)
While my ExpandableView is as follows:
public struct ExpandableView: View {
#State var showExpanded = false
public var body: some View {
VStack(alignment: .leading, spacing: .spacing2) {
HStack(spacing: .spacing2, content: {
Text("Title")
Spacer()
arrowView // The chevron image
})
.contentShape(Rectangle()) // so the complete HStack is tappable
.onTapGesture {
withAnimation(.spring()) {
showExpanded.toggle()
}
}
.animation(nil)
if showExpanded {
Text("The expanded text underneath the title goes here")
.transition(.opacity.combined(with: .flipFromTop))
}
}
}
As you can see in the image, when I expand the second view in the main VStack, the first one animates too, what am I doing wrong here?

In SwiftUI, is there a way to expand and contract a view to show hidden text and buttons in the expanded version of the view?

I'm making a simple task app and using ForEach to populate task rows with the task information from my model. I need a way to animate my task view to open up and reveal some description text and two buttons. I want to turn from A into B on tap, and then back again on tap:
Design Image
I've tried a couple things. I successfully got a proof-of-concept rectangle animating in a test project, but there are issues. The rectangle shrinks and grows from the centre point, vs. from the bottom only. When I place text inside it, the text doesn't get hidden and it looks really bad.
struct ContentView: View {
#State var animate = false
var animation: Animation = .spring()
var body: some View {
VStack {
Rectangle()
.frame(width: 200, height: animate ? 60 : 300)
.foregroundColor(.blue)
.onTapGesture {
withAnimation(animation) {
animate.toggle()
}
}
}
}
In my main app, I was able to replace my first task view (closed) with another view that's open. This works but it looks bad and it's not really doing what I want. It's effectively replacing the view with another one using a fade animation.
ForEach(taskArrayHigh) { task in
if animate == false {
TaskView(taskTitle: task.title, category: task.category?.rawValue ?? "", complete: task.complete?.rawValue ?? "", priorityColor: Color("HighPriority"), task: task, activeDate: activeDate)
.padding(.top, 10)
.padding(.horizontal)
.onTapGesture {
withAnimation(.easeIn) {
animate.toggle()
}
}
.transition(.move(edge: .bottom))
} else if animate == true {
TaskViewOpen(task: "Grocery Shopping", category: "Home", remaining: 204, completed: 4)
.padding(.top, 10)
.padding(.horizontal)
.onTapGesture {
withAnimation(.easeIn) {
animate.toggle()
}
}
}
Is there a way to animate my original closed view to open up and reveal the description text and buttons?
You are on the right track with your .transition line you have, but you want to make sure that the container stays the same and the contents change -- right now, you're replacing the entire view.
Here's a simple example illustrating the concept:
struct ContentView: View {
#State var isExpanded = false
var body: some View {
VStack {
Text("Headline")
if isExpanded {
Text("More Info")
Text("And more")
}
}
.padding()
.frame(maxWidth: .infinity)
.transition(.move(edge: .bottom))
.background(Color.gray.cornerRadius(10.0))
.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}
}
Since you're using it inside a ForEach, you'll probably want to abstract this into its own component, as it'll need its own #State to keep track of the expanded state as I've shown here.
Update, based on comments:
Example of using a PreferenceKey to get the height of the expandable view so that the frame can be animated and nothing fades in and out:
struct ContentView: View {
#State var isExpanded = false
#State var subviewHeight : CGFloat = 0
var body: some View {
VStack {
Text("Headline")
VStack {
Text("More Info")
Text("And more")
Text("And more")
Text("And more")
Text("And more")
Text("And more")
}
}
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
.onPreferenceChange(ViewHeightKey.self) { subviewHeight = $0 }
.frame(height: isExpanded ? subviewHeight : 50, alignment: .top)
.padding()
.clipped()
.frame(maxWidth: .infinity)
.transition(.move(edge: .bottom))
.background(Color.gray.cornerRadius(10.0))
.onTapGesture {
withAnimation(.easeIn(duration: 2.0)) {
isExpanded.toggle()
}
}
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
Using Swift 5 you can use withAnimation and have the view hidden based on state.
ExpandViewer
Has a button to show and hide the inner view
Takes in a content view
struct ExpandViewer <Content: View>: View {
#State private var isExpanded = false
#ViewBuilder let expandableView : Content
var body: some View {
VStack {
Button(action: {
withAnimation(.easeIn(duration: 0.5)) {
self.isExpanded.toggle()
}
}){
Text(self.isExpanded ? "Hide" : "View")
.foregroundColor(.white)
.frame(maxWidth: .infinity, minHeight: 40, alignment: .center)
.background(.blue)
.cornerRadius(5.0)
}
if self.isExpanded {
self.expandableView
}
}
}
}
Using the viewer
ExpandViewer {
Text("Hidden Text")
Text("Hidden Text")
}

Implementing custom sidebar across all views - SwiftUI

OUTLINE
I have made a custom slimline sidebar that I am now implementing across the whole app. The sidebar consists of a main button that is always showing and when pressed it shows or hides the rest of the sidebar that consists of buttons navigating to other views.
I am currently implementing the sidebar across the app on each view by creating a ZStack like this:
struct MainView: View {
var body: some View {
ZStack(alignment: .topLeading) {
SideBarCustom()
Text("Hello, World!")
}
}
}
PROBLEM
I am planning on adding a GeometryReader so if the side bar is shown the rest of the content moves over. With this in mind, the way I am implementing the sidebar on every view feels clunky and a long winded way to add it. Is there a more simple/better method to add this to each view?
Sidebar Code:
struct SideBarCustom: View {
#State var isToggle = false
var names = ["Home", "Products", "Compare", "AR", "Search"]
var icons = ["house.fill", "printer.fill.and.paper.fill", "list.bullet.rectangle", "arkit", "magnifyingglass"]
var imgSize = 20
var body: some View {
GeometryReader { geo in
VStack {
Button(action: {
self.isToggle.toggle()
}, label: {
Image("hexagons")
.resizable()
.frame(width: 40, height: 40)
.padding(.bottom, 20)
})
if isToggle {
ZStack{
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.red)
.frame(width: 70, height: geo.size.height)
VStack(alignment: .center, spacing: 60) {
ForEach(Array(zip(names, icons)), id: \.0) { item in
Button(action: {
// NAVIIGATE TO VIEW
}, label: {
VStack {
Image(systemName: item.1)
.resizable()
.frame(width: CGFloat(imgSize), height: CGFloat(imgSize))
Text(item.0)
}
})
}
}
}
}
}
}
}
}
I don't think there's necessarily a reason to use GeometryReader here. The following is an example that has a dynamic width sidebar (although you could set it to a fixed value) that slides in and out. The main content view resizes itself automatically, since it's in an HStack:
struct ContentView : View {
#State private var sidebarShown = false
var body: some View {
HStack {
if sidebarShown {
CustomSidebar(sidebarShown: $sidebarShown)
.frame(maxHeight: .infinity)
.border(Color.red)
.transition(sidebarShown ? .move(edge: .leading) : .move(edge: .trailing) )
}
ZStack(alignment: .topLeading) {
MainContentView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
if !sidebarShown {
Button(action: {
withAnimation {
sidebarShown.toggle()
}
}) {
Image(systemName: "info.circle")
}
}
}
}
}
}
struct CustomSidebar : View {
#Binding var sidebarShown : Bool
var body: some View {
VStack {
Button(action: {
withAnimation {
sidebarShown.toggle()
}
}) {
Image(systemName: "info.circle")
}
Spacer()
Text("Hi")
Text("There")
Text("World")
Spacer()
}
}
}
struct MainContentView: View {
var body: some View {
VStack {
Text("Main content")
}
}
}

ScrollView navigationlink still active across VStack and GeometryReader, XCode12, beta 4

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 !!
}

How to make a Horizontal List in SwiftUI?

I can wrap all my views inside a List
List {
// contents
}
But this seems to be vertical scrolling. How do I make it horizontal?
You need to add .horizontal property to the scrollview. otherwise it won't scroll.
ScrollView (.horizontal, showsIndicators: false) {
HStack {
//contents
}
}.frame(height: 100)
Starting from iOS 14 beta1 & XCode 12 beta1 you will be able to wrap LazyHStack in a ScrollView to create a horizontal lazy list of items, i.e., each item will only be loaded on demand:
ScrollView(.horizontal) {
LazyHStack {
ForEach(0...50, id: \.self) { index in
Text(String(index))
.onAppear {
print(index)
}
}
}
}
To make a horizontal scrollable content, you can wrap a HStack inside a ScrollView:
ScrollView {
HStack {
ForEach(0..<10) { i in
Text("Item \(i)")
Divider()
}
}
}
.frame(height: 40)
You can use .horizontal property and use custom elements. For me I use cusomt CircleView
var body: some View {
VStack{
Divider()
ScrollView(.horizontal){
HStack(spacing:10){
ForEach(0..<10){
index in
CircleView(label: "\(index)")
}
}.padding()
}.frame(height:100)
Divider()
Spacer()
}
}
}
//struct CircleView:View
#State var label:String
var body:some View {
ZStack{
Circle()
.fill(Color.yellow)
.frame(width: 70, height: 70)
Text(label)
}
}
}

Resources