iOS 16 keyboard safe area not updated on push
Asked Answered
S

4

14

There's a strange keyboard issue on iOS 16, when pushing new screens. It seems the keyboard safe area is not updated when you come back from the pushed screen.

It's even reproducible with this chunk of code on an empty project:

struct ContentView: View {
    
    @State var text = ""
    
    var body: some View {
        NavigationView {
            VStack {
                Spacer()
                NavigationLink {
                    Text("test")
                } label: {
                    Text("Tap me")
                }
                TextField("", text: $text)
                    .textFieldStyle(.roundedBorder)
            }
            .padding()
        }
    }
}

Steps to reproduce:

  • Open the keyboard
  • Press the button "tap me" and navigate to the other screen
  • Quickly come back to the previous screen
  • The keyboard is dismissed, but there's a large gap that fits the keyboard size.

Anyone else had a similar issue?

Springhouse answered 16/9, 2022 at 8:36 Comment(7)
I am having a similar issue with space not being reused after keyboard gets dismissed all over the place, either swiping back but cancelling it midway, or swiping down on a sheet which had keyboard shown. This started happening with iOS 16, even if app was built with iOS 15 SDK.Verbatim
After some debuging, We found: If put textfield in Form (Form { TextField}), bug will be gone. But style is not good for us. If change "formStyle(.columns)", bug again back....Garnett
Fixed on XCode 14.1Homage
@Homage It didn't fix with 14.1 should you do sth else apart from updating Xcode?Exaltation
@Exaltation I used iPhone with iOS 16.1 and xcode 14.1. example: media.giphy.com/media/HwV1P7H8fCpUYN0I6V/giphy.gifHomage
In our case the update to 14.1 does not seem to work either...Frae
Updated to iOS 16.2 and Xcode 14.2 and am still seeing the issue.Leonardoleoncavallo
H
3

I found 2 ways to solve this problem and both will need to hide the keyboard before you go to the next screen

  1. Add hide keyboard to the button which activates navigation to another view
    @State var isActive: Bool = false
    
    var body: some View {
        NavigationView {
            ZStack {
                NavigationLink(isActive: $isActive, destination: { Text("Hello") }, label: EmptyView.init)
                
                VStack {
                    TextField("Text here", text: .constant(""))
                    Button("Press me") {
                        resignFirstResponder()
                        isActive.toggle()
                    }
                }
            }
        }
    }
  1. Add hide keyboard to onChange block
@State var isActive: Bool = false
    
    var body: some View {
        NavigationView {
            ZStack {
                NavigationLink(isActive: $isActive, destination: { Text("Hello") }, label: EmptyView.init)
                    .onChange(of: isActive) { newValue in
                        if newValue {
                            resignFirstResponder()
                        }
                    }
                
                VStack {
                    TextField("Text here", text: .constant(""))
                    Button("Press me") {
                        isActive.toggle()
                    }
                }
            }
        }
    }

Code for hide keyboard:

public func resignFirstResponder() {
    UIApplication.shared.sendAction(
        #selector(UIResponder.resignFirstResponder),
        to: nil,
        from: nil,
        for: nil
    )
}
Homage answered 19/10, 2022 at 11:59 Comment(0)
P
1

Use .ignoreSafeArea(.keyboard) in the parent view and that should solve the issue.

Pargeting answered 1/4, 2023 at 23:6 Comment(0)
V
0

I have found a temporary workaround, it's not pretty but it does the job of removing the empty space that was previously occupied by keyboard. The solution is to call parent view from child in onDisappear, then in parent have a hidden TextField that is focused and almost immediately unfocused.

In parent view add properties:

@State private var dummyText = ""
@FocusState private var dummyFocus: Bool

And put a TextField somewhere in the parent view, inside a ZStack for example:

ZStack {
    TextField("", text: $dummyText)
        .focused($dummyFocus)
        .opacity(0.01)

    ... your other layout ...
}

then call/navigate to the child view with completion block like this:

ChildView(didDismiss: {
    if #available(iOS 16.0, *) {
        dummyFocus = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            dummyFocus = false
        }
    }
})

In child view add property:

var didDismiss: () -> Void

and call the completion block in child's onDisappear:

.onDisappear {
    didDismiss()
}

onDisappear and didDismiss() will only be called after the whole interactive swipe back animation completes. The code checks for iOS 16 so that it doesn't unnecessarily execute on prior versions.

Verbatim answered 26/9, 2022 at 7:37 Comment(0)
F
0

I have come to another fix based on Frin's solution. In my case, all our SwiftUI views are embedded into some parent UIViewController since we have an app that is partially migrated to SwiftUI. What I did is to have a small class (KeyboardLayoutGuideFix) that creates a dummy textfield to capture the focus and then observes the view controller lifecycle to do:

  1. On view disappear: if iOS16, put focus on the dummy textfield
  2. On view appear: remove the focus from dummy textfield

This way, the keyboard layout seems to work as expected, although the keyboard will be dismissed next time you come back to the screen (this is the expected behavior in our case).

Here is the code:

public class KeyboardLayoutGuideFix: Behavior {
    private weak var viewController: UIViewController?
    private lazy var dummyTextField: UITextField = {
        UITextField(frame: .zero).apply { text in
            viewController?.view.addSubview(text)
            text.alpha = 0
        }
    }()
    private var needsEndEditing = false
    private var disposeBag = Set<AnyCancellable>()

    private init(viewController: UIViewController, lifeCycle: ControllerLifeCycle) {
        self.viewController = viewController
        super.init(frame: .zero)
        lifeCycle.$isPresented.sink { [weak self] presented in
            guard let self else { return }
            if presented {
                if self.needsEndEditing {
                    self.needsEndEditing = false
                    DispatchQueue.main.async {
                        self.viewController?.view.endEditing(true)
                    }
                }
            } else {
                self.dummyTextField.becomeFirstResponder()
                self.needsEndEditing = true
            }
        }.store(in: &disposeBag)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public static func apply(viewController: PlaytomicViewController) {
        apply(viewController: viewController, lifeCycle: viewController.lifecycle)
    }

    public static func apply(viewController: UIViewController, lifeCycle: ControllerLifeCycle) {
        if #available(iOS 16, *) {
            let fix = KeyboardLayoutGuideFix(viewController: viewController, lifeCycle: lifeCycle)
            fix.owner = viewController
        }
    }
}

and then use it in the container VC like:

override func viewDidLoad() {
    super.viewDidLoad()
    KeyboardLayoutGuideFix.apply(viewController: self)
}

Note that you will miss the following objects to make this work in your project, but you can adapt it to your own codebase:

  • Behavior: a class that allows you to assign dynamically other objects to a parent one, in this case it assigns the fix to the associated view controller, preventing the deallocation. You can remove it and use a local variable in your VC containing a reference to the fix
  • ControllerLifeCycle: A class that exposes a publisher to track the presentation state of the ViewController. You can replace it by explicit calls in viewWillAppear and viewWillDisappear
  • PlaytomicViewController: Base class that provides the lifecycle and updates the published property when appear/disappear
Frae answered 17/11, 2022 at 8:14 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.