Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI in iOS14 Keyboard Avoidance Issues and IgnoresSafeArea Modifier Issues

Tags:

ios

swiftui

iOS13 saw TextField not having any sort of keyboard avoidance handling in place. As such, we created our how keyboard avoidance mechanism which works well. We upgraded to iOS14 and this resulted in TextFields having keyboard avoidance built in. However, the keyboard avoidance does not seem to work as expected.

Issue 1 The first issue we experienced was keyboard avoidance not working expected for TextFields in and around the centre of the screen. Given this code:

struct ContentView: View {
    
    @State var text:String = ""
    
    var body: some View {
        
        TextField("Testing", text: $text)

    }
}

On an and iPhone 8 Plus the Textfield is moved up. In our opinion this shouldn't be happening as the TextField will not be hidden by the keyboard and as such it should remain in the same place.

Question 1: Is this a bug and should it be reported to Apple? We believe this to be a bug / issue.

We have found that using the following:

struct ContentView: View {
    
    @State var text:String = ""
    
    var body: some View {
        
        VStack {
            Spacer()
            TextField("Testing", text: $text)
        }
        
    }
}

The TextField will be moved just above the keyboard. Which is the expected behaviour. We've also found that with the code below:

struct ContentView: View {
    
    @State var text:String = ""
    
    var body: some View {
        
        VStack {
            TextField("Testing", text: $text)
            Spacer()
        }
        
    }
}

The TextField is not moved as it would never be covered by the TextField. Once again, this is the behaviour we expect. However, any TextField in and around the centre of the screen it would seem that the keyboard avoidance moves the TextField in scenario's where it shouldn't.

Issue 2 Our app holds TextFields in and around the centre on certain screens and as such the issue discovered above simply adds to a poor user experience and so we looked to "switch off" this keyboard avoidance provided to us. We looked to use the ignoresSafeArea modifier as follows:

struct ContentView: View {
    
    @State var text:String = ""
    
    var body: some View {

        if #available(iOS 14.0, *) {
            
            VStack {
                TextField("Testing", text: $text)
            }
            .ignoresSafeArea(.keyboard, edges: .bottom)

            
        } else {
            // Fallback on earlier versions
            // Our iOS13 Code
        }
                
    }
}

The observed result is that the modifier simply doesn't work. The TextField is still moved upwards. However when using something like this:

struct ContentView: View {
    
    @State var text:String = ""
    
    var body: some View {

        if #available(iOS 14.0, *) {
            
            VStack {
                Spacer()
                TextField("Testing", text: $text)
            }
            .ignoresSafeArea(.keyboard, edges: .bottom)

            
        } else {
            // Fallback on earlier versions
            // Our iOS13 Code
        }
                
    }
}

the ignoresSafeArea works and so this leads to the second question:

Question 2 Is there are bug with the ignoresSafeArea modifier as well? Is this something that should be reported?

It would seem that there is an underlying issue with TextFields in and around the centre of the screen?

Question 3 Anyone know of ways around these issues? Because right now it's a huge problem on iOS14. The keyboard avoidance doesn't work and any attempt to try and switch it off doesn't work either.

We are using Xcode 12.0 (12A7209)

Update

We have found that wrapping the TextField in a Geometry Reader that this seems to "switch off" Keyboard Avoidance for a TextField. However, we believe this to be a delightful hack that fixes the problem in one way but then also exposes that Keyboard Avoidance doesn't work in Geometry readers which could be a bug / issue for other people...

struct ContentView: View {
    
    @State var text:String = ""
    
    var body: some View {

        if #available(iOS 14.0, *) {
            
            GeometryReader { _ in
                VStack {
                    Spacer().frame(height:500) //Compensate for other Views in the Stack
                    TextField("Testing", text: $text)
                }
            }
            
        } else {
            // Fallback on earlier versions
            // Our iOS13 Code
        }
                
    }
}
like image 353
user14305195 Avatar asked Sep 19 '20 11:09

user14305195


People also ask

What is keyboard avoidance SwiftUI?

The iOS system keyboard appears whenever a user taps an element that accepts text input. It can be a TextField component from SwiftUI, a UITextField and UITextView from UIKit, or a text input field inside a web view.

How do I move view up when keyboard appears in iOS SwiftUI?

You need to add a ScrollView and set a bottom padding of the size of the keyboard so the content will be able to scroll when the keyboard appears. All you have to do now is to embed your content inside the custom ScrollView .

How do I turn off the keyboard on SwiftUI?

Pure SwiftUI (iOS 15) To dismiss the keyboard, simply set view's focusedField to nil . The return key will dismiss keyboard automatically (since iOS 14).


2 Answers

The behaviors you described are all expected. We need to understand three things:

  1. SwiftUI layout system
  2. Keyboard safe area
  3. IgnoresSafeArea

(1) The most relevant concept in SwiftUI layout system is that views can have a fixed size or a flexible size. For example, a single-lined Text has a fixed size. So a VStack that only has Texts also has a fixed size. Try this

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello")
            Text("World")
        }
        .border(Color.green)
    }
}

You can see the green border only wraps around the texts.

On the other hand, Spacers, Colors, GeometryReaders, etc. have flexible sizes, they tend to expand to occupy all the space available.

(2) When the keyboard is showing, there is a safe area applied on the container. The height of the container will decrease.

(3) The ignoresSafeArea modifier is typically applied on views that have a flexible height. Taking the bottom edge for example, the modifier will have an effect only when the original bottom edge of the view is just aligned with the top edge of the bottom safe area or is covered by the bottom safe area. So if the view's bottom edge is far away from the top edge of the bottom safe area, ignoresSafeArea will have no effect.

Now I'll explain why all your examples have expected behavior.

Example 1

struct ContentView: View {

    @State var text: String = ""

    var body: some View {

        TextField("Testing", text: $text)

    }
}

Behavior: The text field moves up a little when the keyboard shows even if it's not covered by the keyboard.

Reason: The safe area is on the container, when the keyboard shows, the height of the container decreases. Since the text field is placed at the center of the container, it moves up a bit.

Example 2

struct ContentView: View {

    @State var text: String = ""

    var body: some View {

        VStack {
            Spacer()
            TextField("Testing", text: $text)
        }

    }
}

Behavior: When the keyboard shows, the text field moves just above the keyboard.

Reason: There is a Spacer in the VStack, so the VStack will extend its height all the way to the height provided by the container. When the height of the container decreases because of the keyboard, the height of the VStack decreases too.

Example 3

struct ContentView: View {

    @State var text: String = ""

    var body: some View {
        VStack {
            TextField("Testing", text: $text)
        }
        .ignoresSafeArea(.keyboard, edges: .bottom)
    }
}

Behavior: The text field moves up a little when the keyboard shows. The ignoresSafeArea modifier doesn't have any effect.

Reason: The ignoresSafeArea is applied on the VStack, while the VStack has a fixed height, and its bottom edge is far away from the bottom safe area, ignoresSafeArea has no effect. But the container does not ignoresSafeArea, so the height of the container still decreases when the keyboard shows.

Example 4

struct ContentView: View {

    @State var text: String = ""

    var body: some View {
        VStack {
            Spacer()
            TextField("Testing", text: $text)
        }
        .ignoresSafeArea(.keyboard, edges: .bottom)
    }
}

Behavior: The text field does not move when the keyboard shows, ignoresSafeArea is working.

Reason: This time the Spacer will make the VStack extend its height. Keep in mind the container doesn't ignoresSafeArea, so if the ignoresSafeArea modifier were not applied, when the keyboard shows, the bottom edge of the VStack will be just aligned to the top edge of the bottom safe area. So this time if ignoresSafeArea is applied, it will work and makes the VStack extend its height to ignore the keyboard bottom safe area.

Another example may help you understand how a parent view can respect safe areas while a subview can ignore them.

Example 5:

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.yellow
            Color.green
                .frame(width: 200)
                .ignoresSafeArea()
        }
        .border(Color.blue, width: 10)
    }
}

Behavior:

Screen shot

From the blue border, we see the parent ZStack respects safe areas while the green subview ignores safe areas.

Switch off Keyboard Avoidance

To switch off the iOS 14 keyboard avoidance, you can apply ignoresSafeArea to a parent view that extends its height, for example

struct ContentView: View {

    @State var text: String = ""

    var body: some View {
        ZStack {
            Color.clear
            VStack {
                TextField("Testing", text: $text)
            }
        }
        .ignoresSafeArea(.keyboard)
    }
}

In this example, the Color.clear extends its height, making the ZStack extend its height, so the ZStack will ignore the keyboard safe area. The VStack is placed at the center of the ZStack, thus not affected by the keyboard.

like image 177
Cosyn Avatar answered Oct 19 '22 00:10

Cosyn


My situation is that I did not want any keyboard avoidance, and I had multiple hosting controllers inside my main view controller.

Here's how I handled it for the type EditTextOverlayView:

struct EditTextOverlayViewWrapper: View {

    var observed: ObservedTextEditing

    var body: some View {
        GeometryReader { geometry in
            EditTextOverlayView(observing: observed)
        }.ignoresSafeArea(.keyboard, edges: .bottom)
    }
}

This kept everything exactly where it was when the keyboard appeared, allowing me to make my custom adjustment without the keyboard avoidance interfering. (The observed var is just passed through the wrapper; otherwise, the view functions exactly the same as the wrapped EditTextOverlayView.)

like image 22
dwsolberg Avatar answered Oct 19 '22 00:10

dwsolberg