SwiftUI Positioning View [duplicate] - swift

I've seen similar questions like this one here on Stack Overflow, but none of them have been able to answer my question.
I have created a List, but I want to remove the padding space (marked with a red arrow) above the content in the List. How can I remove it when using a GroupedListStyle?
This is a List within a VStack within a NavigationView that is displayed via fullscreenCover:
var body: some View {
NavigationView {
VStack {
taskEventPicker
myList
}
.navigationBarTitle("Add Task", displayMode: .inline)
}
}
where taskEventPicker is a segmented Picker (boxed in green):
var taskEventPicker: some View {
Picker("Mode", selection: $selection) { /* ... */ }
.pickerStyle(SegmentedPickerStyle())
.padding()
}
and myList is the form in question (boxed in yellow):
var myList: some View {
List { /* ... */ }
.listStyle(GroupedListStyle())
}
What I've Tried
Removing the header of the list
Hiding the Navigation Bar title
Setting UITableView.appearance().tableHeaderView to an empty frame
Attempting UITableView.appearance().contentInset.top = -35
Setting listRowInsets (this affects all list rows)
Note: I'm looking for an answer that can apply to the GroupedListStyle. I understand that this issue does not occur with PlainListStyle. This padding issue also occurs with the default listStyle.
Thanks for the help!
Xcode version: 12.5

Firstly, I would say that GroupedListStyle is working as intended.
On iOS, the grouped list style displays a larger header and footer
than the plain style, which visually distances the members of
different sections.
You say you have tried this, but it does work for me (Xcode 12.5.1):
List { ... }
.onAppear(perform: {
UITableView.appearance().contentInset.top = -35
})
You could also hide the list header, by using a ZStack with the List at the bottom of the stack and the Picker over the top. The Picker does have transparency, so you would also have to add an opaque view to act as background for the Picker.
var body: some View {
NavigationView {
ZStack(alignment: .top) {
List { ... }
.listStyle(.grouped)
.padding(.top, 30)
Color.white
.frame(height: 65)
Picker { ... }
.pickerStyle(.segmented)
.padding()
}
.navigationBarTitle("Add Task", displayMode: .inline)
}
}
As far as I can see this just appears the same as PlainListStyle would do, but I assume you have a particular reason for wanting to use GroupedListStyle.

The contentInsent part of the accepted answer works perfectly fine unless for instance as in my case, at the end of a navigation stack you want to use it in some detail view containing a List with navigationBarTitleDisplayMode set to .inline. When navigating back in the stack to a view using another List with navigationBarTitleDisplayMode set to .large, the List content and NavigationBar interlace awfully. And they probably might as well do whatever the navigationBarTitleDisplayMode is set to.
Switching to listStyle(.plain) in my case isn't a feasable option, because then in the detail view, I loose the beautifully animated built-in transitions to and from List's EditMode, that seem to exist only in the grouped listStyle varieties.
Finally, after quite a few days of frustration I figured out that tableHeaderView is the approach that works best for my problem - and of course, it works just as well to solve the original question. It's all in here ...:
XCode documentation
... and in this sentence:
"When assigning a view to this property [i.e. tableHeaderView], set the height of that view to a nonzero value"
So I do like:
List {
// bla bla bla
}
.listStyle(.grouped)
.onAppear {
let tableHeaderView = UIView(frame: .zero)
tableHeaderView.frame.size.height = 1
UITableView.appearance().tableHeaderView = tableHeaderView
}
It's as simple as that. Hope it might help you, too!

Give a header to the first section in your list: Section(header: Spacer(minLength: 0))
Disclaimer: this doesn't totally remove the top spacing, but it does yield the same result as the native Settings app.
Note: This worked for me on iOS 14.5
VStack {
List {
Section(header: Spacer(minLength: 0)) {
Text(verbatim: "First Section")
}
Section {
Text(verbatim: "Second Section")
}
}
.listStyle(GroupedListStyle())
}

Another option is to add
List { ... }.offset(x: 0, y: -30).edgesIgnoringSafeArea(.bottom)
to the List. Since you won't be able to scroll all the way up when using this add a Spacer(minLength: 30) at the very bottom of the List.

I had a similar setup where I had some views and a List in a VStack and I had an undesired space that appeared on the top of my List. In my case I needed to set the VStack spacing to 0 and that solved the issue because it was actually just spacing from the VStack.
VStack(spacing: 0) { ... }

my 2 cents. I arrived here for the same reason.
take a look at:
https://developer.apple.com/forums/thread/662544
it' seems the correct approach.

extension View {
/// Clear tableview and cell color
func tableClearColor() {
let tableHeaderView = UIView(frame: .zero)
tableHeaderView.frame.size.height = 0.5
UITableView.appearance().tableHeaderView = tableHeaderView
}
}

Related

Applying padding to SwiftUI Picker with menu style is causing multiline text

I have a Picker with menu style presenting a label with an icon and text. When applying a padding to any views containing the picker, the picker will behave as though there is not enough space and run to multiple lines even though there is enough space.
Sample Code
struct ContentView: View {
let options = [
"Some Long Text"
]
private var selectedOption: Binding<String> =
.constant("Some Long Text")
var body: some View {
HStack {
Text("Some Text")
Picker("Work Type", selection: selectedOption) {
ForEach(self.options, id: \.self) {
Label($0, systemImage: "pencil.tip.crop.circle")
}
}.pickerStyle(.menu)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
VStack {
ContentView()
ContentView()
.padding()
}
}
}
This might be a layout bug in SwiftUI. There is clearly enough space to put it all in one line, even with the padding. You may want to file a bug report to Apple.
However, you can easily fix this by applying the fixedSize() modifier to your Picker:
Picker("Work Type", selection: selectedOption) {
// ...
}
.pickerStyle(.menu)
.fixedSize(horizontal: true, vertical: false)
This will make sure that the Picker doesn't concern itself with external sizing requirements, ignores the proposed with and just uses renders with its ideal size.
Please note that this might lead to unintended behavior. For example, when the user chooses a bigger font size in accessibility settings, the text in the Picker will never break and might be rendered off-screen. So make sure to think through all your use cases.
PS: You can also use a Menu instead of a Picker with .menu style. Of course, you won't get the selection behavior for free and have to do it manually, but it doesn't have this layout bug.
This seems like intended behavior to me. Because of the padding applied around the HStack (specifically at the horizontal edges) there is not enough space to render the form label on one line. If you were to change the size of the screen (for example using an iPad simulator or using landscape mode) the label would have enough room and be displayed on one line. I tested this code on an iPad simulator and it worked as expected.
If you want to force the label text to be rendered on one line, use fixedSize(horizontal:vertical:). Though this can lead to unexpected behavior.
Picker("Work Type", selection: selectedOption) {
ForEach(self.options, id: \.self) {
Label($0, systemImage: "pencil.tip.crop.circle")
}
}
.pickerStyle(.menu)
.fixedSize(horizontal: true, vertical: false)

Why won't a nested scrollview respond to scrolls in swiftUI?

I'm building an SwiftUI app with a dropdown menu with a vertical ScrollView within another vertical ScrollView. However, the dropdown menu one (the nested one) won't scroll. I would like to give it priority somehow. It seems like a simple problem, but I have scoured the internet but cannot find an adequate solution. Here is the basic code for the problem (the code is cleaner in the app but copy and pasting particular snippets did not work very well):
ScrollView{
VStack{
(other stuff)
DropdownSelector()
(other stuff)
}
}
struct DropdownSelector(){
ScrollView{
VStack(alignment: .leading, spacing: 0) {
ForEach(self.options, id: \.self) { option in
(do things with the option)
}
}
}
Creating nested ScrollViews in the first place is probably a bad idea. Nonetheless, there is a solution.
Because with ScrollView it scrolls as much as the content height, this is a problem when they are nested. This is because the inner ScrollView isn't limited in height (because the outer ScrollView height just changes), so it acts as if it wasn't there at all.
Here is a minimal example demonstrating the problem, just for comparison:
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Text("Top view")
DropdownSelector()
Text("Bottom view")
}
}
}
}
struct DropdownSelector: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(0 ..< 10) { i in
Text("Item: \(i)")
}
}
}
}
}
To fix it, limit the height of the inner scroll view. Add this after DropdownSelector():
.frame(height: 100)

SwiftUI - Adjust ScrollView scroll indicator insets

I am creating a chat app and I want the content of the ScrollView to go beneath the input field (while scrolling up). I already put the ScrollView and the input field in a ZStack. Bottom padding on the ScrollView puts the content up, but I also want the scroll indicator to move up with the content.
Is there any way to change the insets of the scroll indicator to match the padding, or any other workaround to achieve what I'm looking for?
Here's the current code:
ZStack(alignment: .bottom) {
ScrollView {
ScrollViewReader { value in
VStack(spacing: 5) {
ForEach(MOCK_MESSAGES) {
mMessage in
MessageView(mMessage: mMessage)
}
.onAppear {
value.scrollTo(MOCK_MESSAGES.count - 1)
}
}
.padding(.top, 10)
.padding(.bottom, 40)
}
}
MessageInputView(messageText: $messageText)
}
Summing up: The idea is to have the ScrollView to be above the input view, but move the content underneath input view when scrolling up.
iOS 15 / Swift 3
You could do this with .safeAreaInset instead of padding, then it handles the scroll indicator insets for you as well.
ScrollView {
ScrollViewReader { value in
VStack(spacing: 5) {
// stuff
}
}
}
.safeAreaInset(edge: .bottom) {
MessageInputView(messageText: $messageText)
}
Note: .safeAreaInsets doesn't seem to be working on List as of Xcode 13.1.
UPDATE: As of iOS 15.2, safeAreaInsets() works with List and Form as well.
iOS 14 / Swift 2 or with List
Like many things in SwiftUI, there doesn't seem to be a to do it without tinkering with the underlying UIKit components. There's an easy solution for this one though, using the Introspect library:
ZStack(alignment: .bottom) {
ScrollView {
ScrollViewReader { value in
VStack(spacing: 5) {
// stuff
}
.padding(.top, 10)
.padding(.bottom, 40)
}
}
.introspectScrollView { sv in
sv.verticalScrollIndicatorInsets.top = 10
sv.verticalScrollIndicatorInsets.bottom = 40
}
MessageInputView(messageText: $messageText)
}
You could also do it with a List with introspectTableView.

How to disable vertical scroll in TabView with SwiftUI?

I have set up a TabView in my application, so that I can swipe horizontally between multiple pages, but I also have an unwanted vertical scroll that may appear, with a bounce effect so. How can I disable this vertical scroll?
My code:
struct ContentView: View {
#State private var currentTabIndex: Double = 0
var body: some View {
VStack {
TabView(selection: $currentTabIndex) {
Text("Text n°1")
.tag(0)
Text("Text n°2")
.tag(1)
}
.border(Color.black)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
}
I had this same problem. It's not an exact solution, but you can turn off bouncing on scrollviews (which is used within a TabView). And as long as the items within the TabView are not larger than the TabView frame, it should act as if you disabled vertical scrolling.
I would call it either .onAppear or in your init function:
.onAppear(perform: {
UIScrollView.appearance().bounces = false
})
Note: this disables the bouncing on ALL scrollviews across your app... So you may want to re-enable it .onDisappear.
Still an issue with Xcode 12.4.
I managed to workaround that by wrapping the TabView within a ScrollView and using the alwaysBounceVertical property set to false, as follow:
ScrollView(.horizontal) {
TabView {
///your content goes here
}
.tabViewStyle(PageTabViewStyle())
}
.onAppear(perform: {
UIScrollView.appearance().alwaysBounceVertical = false
})
.onDisappear(perform: {
UIScrollView.appearance().alwaysBounceVertical = true
})
I actually came across this because I saw this effect in a tutorial but couldn’t replicate it on iOS 15.2. However, I managed to replicate it on iOS 14.4 on another simulator side by side. So I guess this behaviour is disabled or fundamentally changed in the newer iOS.
Demonstration

HStack inside ScrollView animates weirdly when opening view

So I was trying to implement expanding/contracting rows in a for each embedded in a list view and it turns out the list view cells don't animate smoothly. After checking out this tutorial, it suggests using a scrollview with a foreach because it animates smoother. The expanding/contracting works fine, but there unintended side effects only when I first open the page. The HStack appears to start flattened on the left side of the view and animates expanding to its normal starting position. I've slowed down the animations in the video so it's easier to see, and I removed the ForEach since it doesn't seem to be the cause of the problems. I don't know what is causing this and google searching has yielded no answers. Does anyone here have an answer or at least some advice? Much appreciated
#State var showTemp: Bool = false
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
HStack(alignment: .top) {
Text("Hello")
.font(.system(size: 32))
if showTemp {
VStack {
Text("Middle 1")
Text("Middle 2")
Text("Middle 3")
Text("Middle 4")
Text("Middle 5")
}
}
Spacer()
Text("Goodbye")
}
.border(Color.black, width: 2)
.onTapGesture {
self.showTemp.toggle()
}
.background(Color.blue)
.animation(.default)
}
.background(Color.green)
.onAppear(perform: loadMethod)
}
It seems I've answered my own question after I looked at the problem from a different angle. Using GeogetryReader and setting the frame width of the HStack to geometry.size.width fixed it.
I think this works because without specifying the width, the HStack fills as much area as it can. For some reason, it starts smushed when the view is loaded and then expands to fill its area, which I guess isn't visible unless you apply an animation to it.
Trying to be helpful here, I don't want to be one of those people who answers their own questions but doesn't show what the answer it.