SwiftUI dragging ScrollView flickers as the keyboard is dismissing - ios

When I develop the feature that allows users drag ScrollView to dismiss keyboard in SwiftUI, I find that if you drag ScrollView as the keyboard is dismissing, the ScrollView will flicker. That will destroy the experience of the feature.
Here's the video and minimal code example:
📺 Video
struct ContentView: View {
#State var text:String = ""
var body: some View {
ScrollView {
Rectangle()
.frame(height: 300)
TextField("test", text: $text)
.padding()
.background(Color.gray)
.padding()
}
}
}

It's caused because the keyboard forces the view to resize.
Add
.ignoresSafeArea(.keyboard, edges: .bottom)
to the bottom of your view to solve this issue

Related

Disable keyboard avoidance in a bottom sheet

TLDR: The view modifier .ignoresSafeArea(.keyboard) does not appear to work when used inside a bottom sheet. Is there a workaround?
In a SwiftUI View, tapping a TextField invokes the keyboard and the Textfield then moves upwards to avoid the keyboard.
struct ContentView: View {
#State var mytext: String = "Some text"
var body: some View {
VStack {
Spacer()
TextField("abc", text: $mytext)
Spacer()
}
}
}
This keyboard avoidance behaviour can be disabled by adding the .ignoresSafeArea modifier
struct ContentView: View {
#State var mytext: String = "Some text"
var body: some View {
VStack {
Spacer()
TextField("abc", text: $mytext)
Spacer()
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
and the TextField no longer moves upwards.
If this technique is applied inside to a view in a bottom sheet it no longer works and the entire sheet is pushed up by the keyboard.
struct ContentView: View {
#State var mytext: String = "Some text"
#State var isPresented: Bool = true
var body: some View {
Color.mint
.sheet(isPresented: $isPresented) {
VStack {
Spacer()
TextField("abc", text: $mytext)
Spacer()
}
.presentationDetents( [.fraction(0.33)] )
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
}
I've tried applying .ignoresSafeArea(.keyboard, edges: .bottom) to every view thats exposed in the code with no success.
I suspect that the bug is due to the bottom sheet implementation using a UIHostingController internally. This can been seen using Xcode's Debug View Hierarchy tool.
Others have described how UIHostingController does not respect the .ignoresSafeArea(.keyboard, edges: .bottom) modifier and have developed workarounds but these are not applicable here because the UIHostingController is created internally, not explicitly in my code.
Is there any way to get the view inside the sheet to ignore the keyboard and stay put?
I'm open to any and all suggestions. Thanks!

How to have the frame of a scroll view change as you scroll in SwiftUI

I'm trying to setup an interface like this with a content view on top and a scroll view at the bottom. When you scroll up in scroll view, I want the scroll view's frame to move up until it's fullscreen. Similarly, once the scroll view is fullscreen I want it to come back down when you scroll in the reverse direction. This is similar to the modal sheet behavior with detents, but I want both views to be interactive at the same time. Is this possible?
Here's an example:
struct ContentView: View {
let content = 1...20
var body: some View {
VStack {
VStack {
Spacer()
HStack {
Spacer()
Text("Content View")
Spacer()
}
Spacer()
}
.background(Color(uiColor: .systemGray5))
List {
ForEach(content, id: \.self) { item in
Text("\(item)")
}
}
.frame(height: 300)
.listStyle(.plain)
}
}
}
I want the frame to change as you scroll up.

SwiftUI - TabView automatically changes selection when size changes

I have a paging style TabView in a HStack with another view. I'm adding a selection binding to the TabView to programmatically control selection.
When a tab is tapped the second view in the HStack disappears and the TabView resizes to fill the space.
However, if this is done after scrolling through my TabView's contents what happens is the selection changes many times as the size changes, causing the TabView to erratically scroll through many pages, rather than stay on the same selection and resize.
To reproduce:
Create a new project
Replace ContentView with the following code
Scroll through ~20 pages
Tap on the rectangle (blue or red)
The selection will change by itself.
Tested on iOS 14.5
struct ContentView: View {
#State var showSidebar = true
#State var selection = 0
var body: some View {
HStack {
Rectangle()
.frame(width: showSidebar ? 100 : 0)
.frame(maxHeight: .infinity)
TabView(selection: $selection) {
ForEach(0..<100, id: \.self) { i in
Rectangle()
.foregroundColor(i % 2 == 0 ? .red : .blue)
.padding()
.onTapGesture {
withAnimation {
showSidebar.toggle()
}
}
}
}.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
}

Pulling Down on ScrollView in SwiftUI Popover Closes It

I know that there is a built-in behavior in modals in iOS that when you swipe them down, the modal pulls down and closes. But I'm running into an issue where the slightest downward scroll on a ScrollView in a .popover closes it instantly.
I'm using a .popover in this case because it's a popover in the Mac version of my app, but in iOS it defaults to a modal sheet.
Here's what is happening when I scroll down:
The instant I scroll down, the modal jitters and closes. Here's the sample project that illustrates that:
import SwiftUI
struct ContentView: View {
#State var showModal = false
var body: some View {
ZStack{
Button("Open Sheet"){
showModal = true
}
}
.popover(isPresented: $showModal, arrowEdge: .bottom){
ModalView(showModal: $showModal)
}
}
}
struct ModalView: View{
#Binding var showModal: Bool
var body: some View{
ScrollView{
VStack{
Text("One")
Text("Two")
Text("Three")
}
}
.frame(maxWidth: .infinity)
.padding(20)
.background(Color.gray)
}
}
Is there something I can do to prevent the sheet/modal from closing when I scroll my ScrollView down?
This is a SwiftUI bug. It has been fixed in Xcode 12.5 beta.

SwiftUI- can't drag a view outside of a ScrollView

Here is a very simplified version of my layout. I have a text view that I'm able to drag around...
struct DragView: View {
var text:String
#State var dragAmt = CGSize.zero
var body: some View {
Text(text)
.padding(15)
.background(Color.orange)
.cornerRadius(20)
.padding(5)
.frame(width: 150, height: 60, alignment: .leading)
.offset(dragAmt)
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged {
self.dragAmt = CGSize(width: $0.translation.width, height: $0.translation.height)
}
.onEnded {_ in
self.dragAmt = CGSize.zero
})
}
}
And then I arrange these views like so:
struct ContentView: View {
var body: some View {
HStack {
ScrollView {
VStack {
DragView(text: "hi")
DragView(text: "hi")
DragView(text: "hi")
}
}
Divider()
VStack {
DragView(text: "hi")
DragView(text: "hi")
DragView(text: "hi")
}
}
}
}
The views that are not in the ScrollView act as expected, and I can drag them around the entire screen. The views that are in the ScrollView, however, will disappear if brought outside the bounds of the ScrollView.
Why does this occur? Is there any way to enable these views to be dragged outside the ScrollView?
Why does this occur?
You drag, you just don't see it because ScrollView clips its content view. Stacks do not, by default. If you add .clipped() for bottom VStack you'll see the same.
Is there any way to enable these views to be dragged outside the
ScrollView?
Solution might be using ZStack. Say on drag you hide one view in ScrollView and show same out-of ScrollView at the same global location, so visually it would behave as would drag original.

Resources