How to set contentInset of a ScrollView in SwiftUI - swift

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

Related

Can't show dropdown menu over HStack in SwiftUI

I have a list item that needs to have a dropdown menu that shows on top of the other views in the HStack.
This is what I have currently:
struct ListRow: View {
#Binding var item: Item
#State var showDropdown: Bool = false
var body: some View {
ZStack {
HStack {
VStack {
Text(item.name)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .topLeading)
ProgressView(value: item.percentDone)
}.padding(.all, 10)
Button(action: {
self.showDropdown.toggle()
}, label: {
Image(systemName: "ellipsis.circle")
}).buttonStyle(BorderlessButtonStyle())
.overlay (
VStack {
if self.showDropdown {
Spacer(minLength: 40)
Text("TEST")
.onTapGesture {
print("Delete")
}
}
}.zIndex(10)
)
}
}
}
}
But when I run it and click the button this is what I see
It looks like the Text is not showing due to a view overflow/overlap with the rest of the HStack, but I thought that adding the .zIndex would place the dropdown on top of the rest of the views in the ZStack
Just set frame for your overlay view to make it exceed its main view 's size
.overlay (
VStack {
if self.showDropdown {
Spacer(minLength: 40)
Text("TEST")
.onTapGesture {
print("Delete")
}
}
}.frame(width: 100)
)
Or you can try another approach like ZStack or Menu (as you mentioned a better solution)
I figured out a better solution.
struct ListRow: View {
#Binding var item: Item
var body: some View {
HStack {
VStack {
Text(item.name)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .topLeading)
ProgressView(value: item.percentDone)
}.padding(.all, 10)
Menu {
Button("Delete", action: {
print("Delete")
})
} label: {
}
.menuStyle(BorderlessButtonMenuStyle())
.frame(width: 10, height: 10, alignment: .center)
}
}
}

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"])
}
}

Large space above navigationTitle (Swift / SwiftUI) [duplicate]

Im currently working on a project for iOS using SwiftUI. I have 3 pages (MainMenu, CalendarList, and DateDetails.)
On the 2nd page (CalenderList) there is an empty space between the top of the screen and the actual NavigationBarTitle.
on the third page, you can see the back button (to the MainMenu) and there is two empty spaces at the top.
I've seen people use .navigationBarHidden to fix this, but i haven't been able to implement it in a way that fixes the problem.
Am i using NavigationView() incorrectly? or is there a special trick?
Here is the code for the MainMenu:
import SwiftUI
struct MainMenu: View {
var body: some View {
NavigationView {
VStack {
Text("Calendar")
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(Color(red: 0.055, green: 0.173, blue: 0.322))
.padding(.top, 55.0)
Text("Main Menu")
.font(.headline)
.foregroundColor(Color(red: 0.635, green: 0.635, blue: 0.635, opacity: 1.0))
/*Image("Logo")
.resizable()
.frame(width: 150.0, height: 150.0)*/
Spacer()
HStack {
NavigationLink(destination: CalendarList()) {
Image(systemName: "calendar")
.resizable()
.frame(width: 75.0, height: 75.0)
.padding()
}
NavigationLink(destination: CalendarList()) {
Image(systemName: "gear")
.resizable()
.frame(width: 75.0, height: 75.0)
.padding()
}
}
HStack {
NavigationLink(destination: StudentInfo()) {
Image(systemName: "info.circle")
.resizable()
.frame(width: 75.0, height: 75.0)
.padding()
}
NavigationLink(destination: CalendarList()) {
Image(systemName: "exclamationmark.circle")
.resizable()
.frame(width: 75.0, height: 75.0)
.padding()
}
}
Spacer()
}
}
}
}
Here is the code for CalendarList (page 2):
import SwiftUI
struct CalendarList: View {
var body: some View {
NavigationView {
List(calendarData, id: \.date) { Calendar in
if Calendar.collab {
NavigationLink(destination: DateDetails(calendar: Calendar)) {
CalendarRow(calendar: Calendar)
}
} else {
CalendarRow(calendar: Calendar)
}
}
.navigationBarTitle(Text("Schedule"))
}
}
}
And here is the code for DateDetails (page 3):
import SwiftUI
struct DateDetails: View {
var calendar: Calendar
var body: some View {
NavigationView {
VStack (alignment: .center) {
//Image("Logo")
HStack {
Text(calendar.month.prefix(4) + ".")
.font(.largeTitle)
Text(String(calendar.date).suffix(1))
.font(.largeTitle)
Spacer()
}
HStack {
Text(calendar.schedule)
.font(.title)
Spacer()
}
Spacer()
.frame(height: 30.0)
Text(calendar.info)
.font(.body)
Spacer()
}
.navigationBarTitle(String(calendar.date).prefix(4).suffix(2) + "/" + String(calendar.date).suffix(2))
.padding()
}
}
}
Only use NavigationView at the top level, you don't need to add it in every subscreen, just remove it from CalendarList and DateDetails and it will fix your spacing issue
I think you can delete the NavigationView of DateDetails.
If you want to change the navigationbar, you may want to edit navigationBarItems or change navigationBarHidden to true and customize it.
https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-bar-items-to-a-navigation-view

NavigationBarTitle automatic display mode doesn't work

I am unsure how to fix this. I have implemented a ScrollView and a NavigationView with ZStacks. However, the automatic display mode doesn't work and the ScrollView sits behind it, with the Title overlapping.
https://i.stack.imgur.com/KPkGh.png
https://i.stack.imgur.com/H8NwS.png
Here is my current code:
struct Overview: View {
var body: some View {
ZStack{
NavigationView {
ZStack {
Color.init("Background")
.navigationBarHidden(false)
.navigationBarTitle("Overview", displayMode: .automatic)
.edgesIgnoringSafeArea(.top)
ScrollView(showsIndicators: false) {
VStack(spacing: 10) {
ForEach(0..<10) {
Text("Item \($0)")
.foregroundColor(.white)
.font(.largeTitle)
.frame(width: 340, height: 200)
.background(ColorManager.BoxColor)
.cornerRadius(10)
}
}
.frame(maxWidth: .infinity)
}
}
}
}
}
}
var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(spacing: 10) {
ForEach(0..<10) {
Text("Item \($0)")
.foregroundColor(.blue)
.font(.largeTitle)
.frame(width: 340, height: 200)
.background(Color(.gray))
.cornerRadius(10)
}
}
.frame(maxWidth: .infinity)
}
.navigationBarHidden(false)
.navigationBarTitle("Overview", displayMode: .automatic)
}
}
You had the navigation item in a ZStack. A ZStack is used to display overlapping views on top of each other.
I believe what you really were looking to achieve stacking the title above the scrollView. That would be achieved with VStack. That however, would still be wrong.
Because it's a navigational setting, it goes inline with the NavigationView. So it can be displayed in the NavigationBar. It doesn't need to be apart of any Stacks.

SwiftUI List/Form/ScrollView being clipped when offset

I changed the y offset of a list, and now it's being clipped.
I am trying to make it so that when you scroll, you can partially see the text underneath the Title and buttons at the top of the view. In other words, I want the top section of the screen to be slightly transparent.
I added the offset to the list, so that it didn't overlap with the information at the top.
The image above is with the VStack in my code showing. I thought that the VStack might be getting in the way, so I commented it out and the image below was the result:
Here's my code:
var body: some View {
ZStack {
VStack {
HStack {
Button(action: {self.showAccountView.toggle()}) {
Image(systemName: "person.fill")
.renderingMode(.original)
.font(.system(size: 20, weight: .bold))
.frame(width: 44, height: 44)
.modifier(NavButtons())
}
.sheet(isPresented: $showAccountView) {
AccountView()
}
Spacer()
Button(action: { self.showHelpCenter.toggle()}) {
Image(systemName: "questionmark")
.renderingMode(.original)
.font(.system(size: 20, weight: .bold))
.modifier(NavButtons())
}
.sheet(isPresented: $showHelpCenter) {
HelpCenter()
}
}
.frame(maxWidth: .infinity)
.padding(.horizontal)
Spacer()
}
List {
ForEach (store.allLogs) { thing in
VStack (alignment: .leading) {
HStack {
Text("\(thing.date) , \(thing.time)")
}
Text(thing.notes)
.multilineTextAlignment(.leading)
}
}
}.offset(y: 50)
}
}
EDIT:
This is one possible solution:
struct MyList: UIViewRepresentable {
func makeUIView(context: Context) -> UITableView {
let view = UITableView()
view.clipsToBounds = false
return view
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
}
and then you would use makeUIView and updateUIView to update the cells. This is just messy and it's not really using SwiftUI at that point.
Second Edit
I've found this issue with a scrollView as well as a form:
The black is the background. Here's the code for all three:
Group {
List ((0 ... 10), id: \.self) {
Text("Row \($0)")
}
.offset(y: 200)
.border(Color.blue, width: 3)
.background(Color.black)
ScrollView {
Text("Text")
}
.foregroundColor(.white)
.offset(y: 200)
.border(Color.blue, width: 3)
.background(Color.black)
Form {
Text("Text")
}
.offset(y: 200)
.border(Color.blue, width: 3)
.background(Color.black)
}
Here are the wireframes of a List:
Here are the names of frames that are the same height as the List/Form/ScrollView:
List:
PlainList.BodyContent
ListCore.Container
ListRepresentable
View Host
TableWrapper
UpdateCoalescingTableView
Form:
GroupList.BodyContent
ListCore.Container
ListRepresentable
View Host
TableWrapper
UpdateCoalescingTableView
ScrollView:
ScrollViewBody
SystemScrollView
View Host
HostingScrollView
I guess that my question has changed from "how do I do this..." to "Why is this happening?"
I'm pretty confused about what exactly is going on.
UIScrollView.appearance().clipsToBounds = false
Stick this in the body of AppDelegate.swift -> applicationDidFinishLaunchingWithOptions(). It will make all scroll views across your application unclipped by default.
The problem is that a list has a table view (which is a scroll view) underneath. Scroll views by default are clipped. We just need to change that default.
Using LazyHStack instead of HStack solves the clipping problem.