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.
Related
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!
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
In my app when I navigate to another tab and scroll down the view and gets cut off and the navigation bar doesn't collapse as shown here:
I tried to put .edgesIgnoringSafeArea(.top) but then this happens:
When I press the home button the simulator and navigate back to the app the navigation bar will collapse when scrolling as intended. Is this a bug with Xcode? Currently I'm using XCode 11.4.1, testing on an iPhone 11 Pro Max simulator but the exact same result occurs on my physical iPhone 6s Plus.
EDIT: The code for the tab view is as follows:
import SwiftUI
struct MenuScreen: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection){
ItemsTab().tabItem{
Image(systemName: "phone.fill")
Text("Items")
}.tag(0)
TestTab().tabItem{
Image(systemName: "phone.fill")
Text("Test")
}.tag(1)
}
.navigationBarTitle("Menu")
// .edgesIgnoringSafeArea(.top)
.navigationBarItems(trailing: NavigationLink(destination:ProfileScreen()){Text("Profile")})
// .padding(.top,1)
.navigationViewStyle(DefaultNavigationViewStyle())
// .navigationBarHidden(true)
}
}
struct MenuScreen_Previews: PreviewProvider {
static var previews: some View {
MenuScreen()
}
}
The navigation view is wrapped inside a splash screen like so:
import SwiftUI
struct SplashScreen: View {
#State private var isActive = false
let content = ContentView()
var body: some View {
NavigationView{
VStack{
Text("Loading")
LoopingAnimation()
NavigationLink(destination: content,isActive: $isActive,label: {EmptyView()})
}.onAppear(perform: {
self.goToContentView(time:2.5)
}).navigationBarTitle("My app")
}
}
func goToContentView(time:Double){
DispatchQueue.main.asyncAfter(deadline: .now() + Double(time)){
self.isActive = true
}
}
}
struct SplashScreen_Previews: PreviewProvider {
static var previews: some View {
SplashScreen()
}
}
EDIT 2: I tried to put navigation views inside the tabview as shown here:
import SwiftUI
struct MenuScreen: View {
var body: some View {
TabView{
NavigationView{
ItemsTab().navigationBarTitle("Items")
}.tabItem{
Image(systemName: "house.fill")
Text("Items")
}
NavigationView{
TestTab().navigationBarTitle("Test")
}.tabItem{
Image(systemName: "phone.fill")
Text("Test")
}
}
.navigationBarHidden(true)
// .navigationBarBackButtonHidden(true)
.frame(alignment: .center)
// .edgesIgnoringSafeArea(.top)
.navigationBarItems(trailing: NavigationLink(destination:ProfileScreen()){Text("Profile")})
// .padding(.top)
.navigationViewStyle(DefaultNavigationViewStyle())
}
}
struct MenuScreen_Previews: PreviewProvider {
static var previews: some View {
MenuScreen()
}
}
But while it resulted in a collapsing navigation bar that worked even switching tabs the result looked like this:
Made the navigation bar stay collapsed in the tab screen similar to what happens when you navigate to other categories within a tab in the app store by setting .displayMode inside .navigationBarTitle to .inline
Screenshot:
So I want to use this thing to make my view scroll up when editing a TextField (it's a TabbedView App btw).
However the animation didn't work unless I embedded it into a scroll view. This somehow broke my App.
So I get a Thread 1: EXC_BAD_ACCESS (code=1, address=0x8) in my AppDelegate now each time when I try to switch to another tab from the start of my App. I had a similar issue earlier where it I couldn't add my environmentObject() to my TabbedView directly but had to add it to the ContentView in the SceneDelegate. So this time my App works if I remove all my #State var for some reason.. can you see anything that is wrong with that?
struct Home: View {
//#State var isSheetPresented = false //if this one isn't commented out it breaks my tabbed-view app
#State var comment: String = "" //this one doesn't break everything?!?
#EnvironmentObject var someStore: SomeStore //works fine as well
var body: some View {
NavigationView {
ScrollView{ //This is THE ScrollView without everything else works (besides the animation)
VStack(spacing: 20) {
Text("Hi!")
}
}
}
}
}
//--------------------
struct ContentView : View {
#State private var selection = 0
var body: some View {
TabbedView(selection: $selection){
Home()
.tabItem{
Image(systemName: "house")
Text("Home")
}
.tag(0)
Text("Tab # 2")
.tabItem{
Image(systemName: "gear")
Text("#2")
}
.tag(1)
}
}
}
So yea, if you experienced sth. similar or have any idea what is going on please let me know....
I tried running my app in landscape on an iPhone XR simulator and got a blank screen.
The code below is my test. It works correctly on an iPhone 8 simulator and also not the iPhone XR simulator if I remove the NavigationView.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { gp in
VStack(alignment: HorizontalAlignment.center) {
Text("Width: \(gp.size.width)")
Text("Height: \(gp.size.height)")
}
}
}
}
}
I expect that I will see the size of the screen in both landscape and portrait.
Does anyone have any experience with this combination?
There is nothing wrong. It is just that when a big iPhone is in landscape, its horizontal size class is set to .regular, instead of .compact. Think of it, as if it were an iPad.
You can verify it, by sliding from the left size of your screen:
If you change your code to add a default view when nothing is selected, you get this other look:
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { gp in
VStack(alignment: HorizontalAlignment.center) {
Text("Width: \(gp.size.width)")
Text("Height: \(gp.size.height)")
NavigationLink(destination: Text("Something got selected")) { Text("Select something") }
}
}
Text("No Selection")
}
}
}
And if you want to force it to .compact, you do the following:
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { gp in
VStack(alignment: HorizontalAlignment.center) {
Text("Width: \(gp.size.width)")
Text("Height: \(gp.size.height)")
NavigationLink(destination: Text("Something got selected")) { Text("Select something") }
}
}
Text("No Selection")
}.environment(\.horizontalSizeClass, .compact)
}
}
Adding this modifier to the NavigationView works disabling master - detail views.
NavigationView {
// Code
}
.environment(\.horizontalSizeClass, .compact)
Another way to solve this is to force the NavigationView to stack style.
NavigationView {
Text("Hello World")
}.navigationViewStyle(StackNavigationViewStyle())
Or the newer:
NavigationView {
Text("Hello World")
}.navigationViewStyle(.stack)