Why is LazyHGrid not as high as its items? - swift

I've got a LazyHGrid that shows multiple Text views in one row. That LazyHGrid is inside a VStack. However, the LazyHGrid is higher than it needs to be. What causes the extra space?
struct ContentView: View {
let rows = [GridItem()]
var body: some View {
VStack {
ScrollView(.horizontal) {
LazyHGrid(rows: rows) {
ForEach(0..<10) { index in
Text("Test \(index)")
.padding()
.foregroundColor(Color.white)
.background(Color.black)
}
}
}
.background(Color.green)
ScrollView(.vertical) {
VStack {
ForEach(0..<100) { index in
Text(String(index))
.frame(maxWidth: .infinity)
}
}
}
.background(Color.blue)
}
}
}

You just have to add .fixedSize() modifier to your grid, and it works...
LazyHGrid(rows: rows) {
ForEach(0..<10) { index in
Text("Test \(index)")
.padding()
.foregroundColor(Color.white)
.background(Color.black)
}
}
.fixedSize()

This works: Reading the height of the cells with an overlay with GeometryReader
struct ContentView: View {
let rows = [ GridItem() ]
#State private var contentSize = CGFloat.zero
var body: some View {
VStack {
ScrollView(.horizontal) {
LazyHGrid(rows: rows) {
ForEach(0..<10) { index in
Text("Test \(index)")
.padding()
.foregroundColor(.white)
.background(.black)
.overlay(
GeometryReader { geo in
Color.clear.onAppear {
contentSize = geo.size.height
}
}
)
}
}
}
.frame(height: contentSize + 20)
.background(Color.green)
ScrollView(.vertical) {
VStack {
ForEach(0..<100) { index in
Text(String(index))
.frame(maxWidth: .infinity)
}
}
}
.background(Color.blue)
}
}
}

Related

How to do a "reveal"-style collapse/expand animation in SwiftUI?

I'd like to implement an animation in SwiftUI that "reveals" the content of a view to enable expand/collapse functionality. The content of the view I want to collapse and expand is complex: It's not just a simple box, but it's a view hierarchy of dynamic height and content, including images and text.
I've experimented with different options, but it hasn't resulted in the desired effect. Usually what happens is that when I "expand", the whole view was shown right away with 0% opacity, then gradually faded in, with the buttons under the expanded view moving down at the same time. That's what happened when I was using a conditional if statement that actually added and removed the view. So that makes sense.
I then experimented with using a frame modifier: .frame(maxHeight: isExpanded ? .infinity : 0). But that resulted in the contents of the view being "squished" instead of revealed.
I made a paper prototype of what I want:
Any ideas on how to achieve this?
Something like this might work. You can modify the height of what you want to disclose to be 0 when hidden or nil when not so that it'll go for the height defined by the views. Make sure to clip the view afterwards so the contents are not visible outside of the frame's height when not disclosed.
struct ContentView: View {
#State private var isDisclosed = false
var body: some View {
VStack {
Button("Expand") {
withAnimation {
isDisclosed.toggle()
}
}
.buttonStyle(.plain)
VStack {
GroupBox {
Text("Hi")
}
GroupBox {
Text("More details here")
}
}
.frame(height: isDisclosed ? nil : 0, alignment: .top)
.clipped()
HStack {
Text("Cancel")
Spacer()
Text("Book")
}
}
.frame(maxWidth: .infinity)
.background(.thinMaterial)
.padding()
}
}
No, this wasn't trying to match your design, either. This was just to provide a sample way of creating the animation.
Consider the utilization of DisclosureGroup. The following code should be a good approach to your idea.
struct ContentView: View {
var body: some View {
List(0...20, id: \.self) { idx in
DisclosureGroup {
HStack {
Image(systemName: "person.circle.fill")
VStack(alignment: .leading) {
Text("ABC")
Text("Test Test")
}
}
HStack {
Image(systemName: "globe")
VStack(alignment: .leading) {
Text("ABC")
Text("X Y Z")
}
}
HStack {
Image(systemName: "water.waves")
VStack(alignment: .leading) {
Text("Bla Bla")
Text("123")
}
}
HStack{
Button("Cancel", role: .destructive) {}
Spacer()
Button("Book") {}
}
} label: {
HStack {
Spacer()
Text("Expand")
}
}
}
}
The result looks like:
I coded this in under 5 minutes. So of course the design can be optimized to your demands, but the core should be understandable.
import SwiftUI
struct TaskViewCollapsible: View {
#State private var isDisclosed = false
let header: String = "Review Page"
let url: String
let tasks: [String]
var body: some View {
VStack {
HStack {
VStack(spacing: 5) {
Text(header)
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.black)
.padding(.top, 10)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
Text(url)
.font(.system(size: 12, weight: .regular))
.foregroundColor(.black.opacity(0.4))
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer()
Image(systemName: self.isDisclosed ? "chevron.up" : "chevron.down")
.padding(.trailing)
.padding(.top, 10)
}
.onTapGesture {
withAnimation {
isDisclosed.toggle()
}
}
FetchTasks()
.padding(.horizontal, 20)
.padding(.bottom, 5)
.frame(height: isDisclosed ? nil : 0, alignment: .top)
.clipped()
}
.background(
RoundedRectangle(cornerRadius: 8)
.fill(.black.opacity(0.2))
)
.frame(maxWidth: .infinity)
.padding()
}
#ViewBuilder
func FetchTasks() -> some View {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ForEach(0 ..< tasks.count, id: \.self) { value in
Text(tasks[value])
.font(.system(size: 16, weight: .regular))
.foregroundColor(.black)
.padding(.vertical, 0)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.frame(maxHeight: CGFloat(tasks.count) * 20)
}
}
struct TaskViewCollapsible_Previews: PreviewProvider {
static var previews: some View {
TaskViewCollapsible(url: "trello.com", tasks: ["Hello", "Hello", "Hello"])
}
}

Boundary on top of NavigationView

When I was trying to change the color of my navigation view page I realized that there is a weird boundary on top. I can't figure out what it is or how to get rid of it. Would anyone happen to know?
Here is the code.
Image with displayMode: .inline
The parent view code is the view that is presenting the page I am having trouble with.
Parent View:
Code:
import Foundation
import SwiftUI
import UIKit
struct ContentView: View {
// variable for view model
#ObservedObject var viewModel = VariableViewModel()
// SWIFT UI START
var body: some View {
// Main page
NavigationView {
ZStack {
Color(.orange).edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
VStack {
HStack {
Spacer()
NavigationLink(destination:
SettingsView()
){
Image(systemName: "gearshape.fill").font(.system(size: 25))
}
Spacer()
Spacer()
Spacer()
Spacer()
Spacer()
Spacer()
NavigationLink(destination:
Text("You")
){
Image(systemName: "chart.bar.xaxis").font(.system(size: 25))
}
Spacer()
}
Text("Pick a mode!").font(.largeTitle).bold().offset(x: 0, y: 30)
ZStack {
VStack {
Spacer()
// ADDITION SECTION
NavigationLink(destination:
VStack {
Spacer()
MathView(operatorName: "Addition")
}
){
HStack {
Text("Addition")
Image(systemName: "plus.square")
}.font(.largeTitle)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.padding(10)
.border(Color.blue, width: 5)
}
Spacer()
// SUBTRACTION SECTION
NavigationLink(destination:
VStack {
Spacer()
MathView(operatorName: "Subtraction")
}
){
HStack {
Text("Subtraction")
Image(systemName: "minus.square")
}.font(.largeTitle)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.padding(10)
.border(Color.blue, width: 5)
}
Spacer()
// MULTIPLICATION SECTION
NavigationLink(destination:
VStack {
Spacer()
MathView(operatorName: "Multiplication")
}
){
HStack {
Text("Multiplication")
Image(systemName: "multiply.square")
}.font(.largeTitle)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.padding(10)
.border(Color.blue, width: 5)
}
Spacer()
// DIVISION SECTION
NavigationLink(destination:
VStack {
Spacer()
MathView(operatorName: "Division")
}
){
HStack {
Text("Division")
Image(systemName: "divide.square")
}.font(.largeTitle)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.padding(10)
.border(Color.blue, width: 5)
}
}
}.navigationBarHidden(true)
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Code:
import SwiftUI
struct MathView: View {
#ObservedObject var viewModel = VariableViewModel()
let operatorName: String
var body: some View {
ZStack {
Color.orange.edgesIgnoringSafeArea(.all)
VStack {
Spacer()
NavigationLink(destination:
MathContentView(operatorName: "Addition", operatorSymbol: "plus", difficultyNumber1: 5, difficultyNumber2: 5)
) {
Text("Easy")
.font(.title2)
.padding(35)
.foregroundColor(.white)
.background(Color(.systemGreen))
.cornerRadius(40)
.onAppear(perform: {
if operatorName == "Addition" {
self.viewModel.result = self.viewModel.num1 + self.viewModel.num2
} else if operatorName == "Subtraction" {
self.viewModel.result = self.viewModel.num1 - self.viewModel.num2
} else if operatorName == "Multiplication" {
self.viewModel.result = self.viewModel.num1 * self.viewModel.num2
};
withAnimation {
viewModel.resetVariables()
// numbers generator
}
})
}
Spacer()
}
}
}
}
To remove the empty space below the NavigationView add .navigationBarTitleDisplayMode(.inline) to the top view:
ZStack {
// ...
}
.navigationBarTitleDisplayMode(.inline)
Then, the slim line between the navigationViewTitle and the content below comes from the Spacer at the top of the VStack in NavigationLink that pushes the MathView.
NavigationLink(destination:
VStack {
Spacer() // this causes the *slim line*
MathView(operatorName: "Addition")
}
)
You need to remove the Spacer (and the VStack as well):
NavigationLink(destination:
MathView(operatorName: "Addition")
)

How to center align a Section item in a Form

I am trying to center align an array of views inside a section, however all I get is leading alignment. I tried using VStack instead of HStack, and using .frame(width: ..., alignment: .center) but these don't work for me. If I substitute the ScrollView with a Text then that works. I'm using Xcode 12.3 and iOS 14.3.
struct TestSectionAlign : View {
var body : some View {
Form {
Section(header: Text("background color")) {
viewForBackgroundColor
}
}
}
var viewForBackgroundColor : some View {
HStack {
Spacer()
ScrollView.init(.horizontal, showsIndicators: false) {
HStack {
Spacer()
ForEach(0..<15) { i in
Button(action: {
// do something
}) {
Image(systemName: "circle.fill").foregroundColor(Color.init(white: Double(i)/16.0))
}
.buttonStyle(PlainButtonStyle())
}
Spacer()
}
}
Spacer()
}
}
}
Here is a solution using GeometryReader, then setting manually width of the HStack and aligning it center
struct ContentView : View {
var body : some View {
Form {
Section(header: Text("background color")) {
viewForBackgroundColor
}
}
}
var viewForBackgroundColor : some View {
GeometryReader { geometry in
HStack(alignment: .center) {
ScrollView.init(.horizontal, showsIndicators: false) {
HStack(alignment: .center) {
ForEach(0..<15) { i in
Button(action: {
// do something
}) {
Image(systemName: "circle.fill").foregroundColor(Color.init(white: Double(i)/16.0))
}
.buttonStyle(PlainButtonStyle())
}
}.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center) //<< here now center
}
}.border(Color.red)
}
}
}

How to set contentInset of a ScrollView in SwiftUI

I can't seem to find how to set the contentInset of a ScrollView. My goal is to make the last object in my ScrollView above the Purple Main Button.
https://i.stack.imgur.com/QfjZb.jpg
If there is a command, could someone help how to implement this into my current code below. I would appreciate your help!
struct Overview: View {
var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(spacing: 10) {
ForEach(0..<5) {
Text("Item \($0)")
.foregroundColor(.white)
.font(.largeTitle)
.frame(width: 340, height: 200)
.background(Color("Boxes"))
.cornerRadius(10)
}
}
.frame(maxWidth: .infinity)
}
.navigationBarHidden(false)
.navigationBarTitle("Overview", displayMode: .automatic)
}
}
}
Just add a padding to the VStack embedded inside the ScrollView.
Using paddings with the embedded stacks provides the same behaviour as content insets.
ScrollView(showsIndicators: false) {
VStack(spacing: 10) {
// Some content //
}
.padding(.bottom, 200) // Choose a value that works for you
}
You could put an invisible view underneath your ScrollView content and give it bottom padding.
For example with Color.clear and a bottom-padding of 300.
struct Overview: View {
var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(spacing: 10) {
ForEach(0..<5) {
Text("Item \($0)")
.foregroundColor(.white)
.font(.largeTitle)
.frame(width: 340, height: 200)
.background(Color("Boxes"))
.cornerRadius(10)
}
Color.clear.padding(.bottom, 300)
}
.frame(maxWidth: .infinity)
}
.navigationBarHidden(false)
.navigationBarTitle("Overview", displayMode: .automatic)
}
}
}
iOS 15's SwiftUI now has a safeAreaInset modifier.
It's also correctly adjusts scroll bars.
ScrollView(.vertical, showsIndicators: true) {
// scrollview's content
}
.safeAreaInset(edge: .bottom, spacing: 0) {
// some bottom-sticked content, or just –
Spacer()
.frame(height: 44)
}
In iOS 15 I just add negative padding to the VStack.
struct Overview: View {
var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(spacing: 10) {
ForEach(0..<5) {
Text("Item \($0)")
.foregroundColor(.white)
.font(.largeTitle)
.frame(width: 340, height: 200)
.background(Color("Boxes"))
.cornerRadius(10)
}
.padding(.top, -50)
}
.frame(maxWidth: .infinity)
}
.navigationBarHidden(false)
.navigationBarTitle("Overview", displayMode: .automatic)
}
}
}
This article has a pretty good solution that seems to work on iOS13+ and calculates the correct inset automatically so you don't need to guess. It looks like it also doesn't handle the scroll indicator though.
You just need to add some Stack
struct Overview: View {
var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(spacing: 10) {
VStack {}.frame(width: 16, height: 40)
ForEach(0..<5) {
Text("Item \($0)")
}
VStack {}.frame(width: 16, height: 40)
}
}
.navigationBarHidden(false)
.navigationBarTitle("Overview", displayMode: .automatic)
}
}
}

SwiftUI - custom TabBar height based on device

I am trying to set up the height of my custom TabBar based on Device, my code:
struct MyTabBar: View {
#Binding var index: Int
var body: some View {
HStack {
Button(action: {
self.index = 0
}) {
Image(ImageText.iconHome.image)
}
Spacer(minLength: 0)
Button(action: {
self.index = 1
}) {
Image(ImageText.iconBell.image)
}
Spacer(minLength: 0)
Button(action: {
self.index = 2
}) {
Image(ImageText.iconAdd.image)
}
Spacer(minLength: 0)
Button(action: {
self.index = 3
}) {
Image(ImageText.iconSearch.image).foregroundColor(Color.red)
}
Spacer(minLength: 0)
Button(action: {
self.index = 4
}) {
Image(ImageText.iconHamburger.image)
}
}.padding(.horizontal, 26).frame(height: 56)
}
}
If the devices have notch, how can I set my custom TabBar to have higher height? Does SwiftUI have something that can be useful here?
You can use the GeometryReader element to access the safe area insets, and add it as a padding using .safeAreaInsets.bottom (note that the Xcode autocompletion is almost never working on GeometryProxy properties):
var body: some View {
GeometryReader { proxy in
VStack {
// Content goes here
Spacer()
// Custom tab bar
HStack {
Spacer()
Button(action: {}) {
Image(systemName: "house.fill")
.padding()
}
Spacer()
Button(action: {}) {
Image(systemName: "house.fill")
.padding()
}
Spacer()
Button(action: {}) {
Image(systemName: "house.fill")
.padding()
}
Spacer()
Button(action: {}) {
Image(systemName: "house.fill")
.padding()
}
Spacer()
}
.padding(.bottom, proxy.safeAreaInsets.bottom)
.background(Color.red)
}
}.edgesIgnoringSafeArea(.bottom)
}