edgesIgnoringSafeArea(.all) breaks keyboard responder, SwiftUI - swift

Keyboard responder file looks like:
class KeyboardResponder: ObservableObject {
#Published var currentHeight: CGFloat = 0
var _center: NotificationCenter
init(center: NotificationCenter = .default) {
_center = center
_center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
_center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
#objc func keyBoardWillShow(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
withAnimation {
currentHeight = keyboardSize.height
}
}
print("the KEYBOARD HEIGHT IS \(self.currentHeight)")
}
#objc func keyBoardWillHide(notification: Notification) {
withAnimation {
currentHeight = 0
}
print("the KEYBOARD HEIGHT IS \(self.currentHeight)")
}
}
I try to use it in a view where the body is:
VStack {
VStack {
\\view content here
}.offset(y: -self.keyboardResponder.currentHeight) \\ keyboardResponder is an instance of KeyboardResponder
}.edgesIgnoringSafeArea(.all)
When I remove edgesIgnoringSafeArea(.all) it works fine but if I put it in, it breaks the offset so it no longer moves the content at all...

They deprecated .edgesIgnoreSafeArea in iOS 14. The new method has multiple options for the “types” of safe area to ignore: .container (the usual “safe area”), .keyboard (new!), and .all (ignores both container and keyboard — I suspect that’s the behavior you’re getting).
Try .ignoresSafeArea(.container) instead.
https://developer.apple.com/documentation/swiftui/offsetshape/ignoressafearea(_:edges:)

Related

Retrieving keyboard height when textfield editing starts in SwiftUI

So I have a class with a published variable called keyboardHeight that is used to retrieve the value of the keyboardHeight:
import UIKit
class KeyboardHeightHelper: ObservableObject {
#Published var keyboardHeight: CGFloat = 0
init() {
self.listenForKeyboardNotifications()
}
private func listenForKeyboardNotifications() {
NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification,
object: nil,
queue: .main) { (notification) in
guard let userInfo = notification.userInfo,
let keyboardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
self.keyboardHeight = keyboardRect.height
}
NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidHideNotification,
object: nil,
queue: .main) { (notification) in
self.keyboardHeight = 0
}
}
}
Then, in ContentView I just have a TextField that should print the keyboard height when you start/stop editing the field:
import SwiftUI
struct ContentView: View {
#State var textFieldText = ""
#ObservedObject var keyboardHeightHelper = KeyboardHeightHelper()
var body: some View {
VStack {
TextField("Text field",
text: $textFieldText, onEditingChanged: { _ in print("the keyboard height is \(self.keyboardHeightHelper.keyboardHeight)") })
}
}
}
The problem I have is this: When I am not editing the textfield and then click it, it prints the keyboard height is 0.0 (I guess this is because it grabs the keyboardHeight value before it presents the keyboard, so at the time the height is 0.0 as the keyboard isn't seen). When I press return and the keyboard is dismissed, the height of the keyboard (for the iPhone 8 simulator) is printed as the correct value of 260.0. My question is how do I access the value of the keyboard when I start editing?
Try this. You can integrate it using just a modifier to SwiftUI view:
extension UIResponder {
static var currentFirstResponder: UIResponder? {
_currentFirstResponder = nil
UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder(_:)), to: nil, from: nil, for: nil)
return _currentFirstResponder
}
private static weak var _currentFirstResponder: UIResponder?
#objc private func findFirstResponder(_ sender: Any) {
UIResponder._currentFirstResponder = self
}
var globalFrame: CGRect? {
guard let view = self as? UIView else { return nil }
return view.superview?.convert(view.frame, to: nil)
}
}
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { $0.keyboardHeight }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
extension Notification {
var keyboardHeight: CGFloat {
return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
}
}
struct KeyboardAdaptive: ViewModifier {
#State private var bottomPadding: CGFloat = 0
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.padding(.bottom, self.bottomPadding)
.onReceive(Publishers.keyboardHeight) { keyboardHeight in
let keyboardTop = geometry.frame(in: .global).height - keyboardHeight
let focusedTextInputBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0
self.bottomPadding = max(0, focusedTextInputBottom - keyboardTop - geometry.safeAreaInsets.bottom)
}
.animation(.easeOut(duration: 0.16))
}
}
}
extension View {
func keyboardAdaptive() -> some View {
ModifiedContent(content: self, modifier: KeyboardAdaptive())
}
}
Usage:
struct ContentView: View {
#State private var text = ""
var body: some View {
VStack {
Spacer()
TextField("Enter something", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding()
.keyboardAdaptive()//note this
}
}
Credits to multiple online sources.
The .onEditingChanged is not appropriate place to read keyboard height, because you receive this callback right in the moment of click in TextField, so there is no keyboard yet shown (and, so, no notification received).
Instead you can listen explicitly for your keyboardHeight property publisher and be notified exactly when it is changed (what is performed on keyboard notifications synchronously, so in time)
Here is a solution (tested with Xcode 12 / iOS 14)
VStack {
TextField("Text field",
text: $textFieldText, onEditingChanged: { _ in })
.onReceive(keyboardHeightHelper.$keyboardHeight) { value in
print("the keyboard height is \(value)")
}
}

How would I apply the easing animation from the keyboardWillShowNotification to SwiftUI's .animation modifier?

In my SwiftUI layout, when the keyboard becomes visible it moves my textfields and buttons above the keyboard so nothing gets blocked. This is working fine, but I am not currently able to apply the easing to the animation like I can with the duration.
Here is my view
struct ContentView: View {
#State private var name = ""
#ObservedObject private var keyboard = KeyboardResponder()
var body: some View {
VStack(spacing: 0) {
TextField("Fill in the restaurant name", text: $name)
.padding(.all)
TextField("Fill in the restaurant name", text: $name)
.padding(.all)
}
.padding(.bottom, keyboard.currentHeight)
.animation(.easeOut(duration: keyboard.duration)) //trying to apply the easing I grabbed from my keyboard ObservableObject here
}
}
Here is my the keyboard responder i am using to animate the views when needed
final class KeyboardResponder: ObservableObject {
private var notificationCenter: NotificationCenter
#Published private(set) var currentHeight: CGFloat = 0
var duration: TimeInterval = 0
var curve: Int = 0
init(center: NotificationCenter = .default) {
notificationCenter = center
notificationCenter.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
deinit {
notificationCenter.removeObserver(self)
}
#objc func keyBoardWillShow(notification: Notification) {
duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval
curve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int //how can I add this to my animation modifier
print("~~~~> curve \(curve)")
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
currentHeight = keyboardSize.height
}
}
#objc func keyBoardWillHide(notification: Notification) {
duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval
curve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int
currentHeight = 0
}
}
in the keyBoardWillShow notification I can grab the easing, but from there I can't figure out how to apply that to the .animation modifier in the view. The duration was easy enough to apply. Maybe I need to make my own animation type? I am not even sure this is possible yet.
.animation(.easeOut(duration: keyboard.duration))
In UIKit there is an options parameter in UIView.animate where you can apply the easing.
UIView.animate(withDuration: TimeInterval(duration), delay: 0, options: [UIViewAnimationOptions(rawValue: UInt(curve))], animations: {
self.view.layoutIfNeeded()
}, completion: nil)```

How to get List Interactive keyboard height in SwiftUI?

In SwiftUI we can let the keyboard be interactive in a List by swiping down. We only need to provide this code to the init() of the view.
init() {
UITableView.appearance().keyboardDismissMode = .interactive
}
This works fine but the view that is attached to the keyboard isn't moving with the keyboard itself.
I found this post which should solve the issue in UIKit. How to I solve it in SwiftUI?
This is how I handle Keyboard popping up:
final class KeyboardResponder: ObservableObject {
private var _center: NotificationCenter
#Published private(set) var currentHeight: CGFloat = 0
#Published private(set) var duration: Double = 0.0
init(center: NotificationCenter = .default) {
_center = center
_center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
_center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
deinit {
_center.removeObserver(self)
}
#objc func keyBoardWillShow(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double {
self.duration = duration
currentHeight = keyboardSize.height
}
}
#objc func keyBoardWillHide(notification: Notification) {
currentHeight = 0
}
}
struct KeyboardHandler: ViewModifier {
let height: CGFloat
let duration: Double
func body(content: Content) -> some View {
content
.padding(.bottom, height)
.animation(.spring(blendDuration: duration))
}
}

Find out when UIKeyboard.frame intersects with other frame?

I need to find out when the textfield becomes the first responder to notify me whether the keyboard that's going to show will obstruct the UITextField. If it does, I wanna adjust the scrollview properties.
So far I have this setup. I'm listening for UIKeyboardWillShow notifications that calls the following selector:
func keyboardWillAppear(notification:NSNotification)
{
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
{
if keyboardSize.intersects(textField.frame)
{
print("It intersects")
}
else
{
print("Houston, we have a problem")
}
}
Note: I tried with UIKeyboardDidShow but still no success. UITextField is a subview of the scrollView.
listen to size changes of the keyboard
CONVERT the coordinates
working sample:
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
//keyboard observers
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
func keyboardWillChange(notification:NSNotification)
{
print("Keyboard size changed")
if let keyboardSize = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect {
//convert gotten rect
let r = self.view.convert(keyboardSize, from: nil)
//test it
if r.intersects(textView.frame) {
print("intersects!!!")
}
}
}
How about comparing the start position of the keyboard with the end position of the text?
working sample:
func keyboardWillAppear(notification:NSNotification)
{
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
{
if keyboardSize.origin.y < textField.frame.origin.y + textField.frame.size.height {
print("It intersects")
} else {
print("Houston, we have a problem")
}
}
}

Get the frame of the keyboard dynamically

Is it possible to get the frame, actually its height, of the keyboard dynamically? As I have a UITextView and I would like to adjust its height according to the keyboard frame height, when the input method of the keyboard is changed. As you know, different input methods may have different keyboard frame height.
try this:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification
object:nil];
- (void)keyboardWasShown:(NSNotification *)notification
{
// Get the size of the keyboard.
CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
//Given size may not account for screen rotation
int height = MIN(keyboardSize.height,keyboardSize.width);
int width = MAX(keyboardSize.height,keyboardSize.width);
//your other code here..........
}
Tutorial for more information
Just follow this tutorial from Apple and you will get what you want. Apple Documentation. In order to determine the area covered by keyboard please refer to this tutorial.
For the Swift 3 users, the #Hector code (with some additions) would be:
In your viewDidLoad add the observer :
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidShow(_:)), name: .UIKeyboardDidShow , object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidHide(_:)), name: .UIKeyboardDidHide , object: nil)
Then implement those methods:
func keyboardDidShow(_ notification: NSNotification) {
print("Keyboard will show!")
// print(notification.userInfo)
let keyboardSize:CGSize = (notification.userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue.size
print("Keyboard size: \(keyboardSize)")
let height = min(keyboardSize.height, keyboardSize.width)
let width = max(keyboardSize.height, keyboardSize.width)
}
func keyboardDidHide(_ notification: NSNotification) {
print("Keyboard will hide!")
}
You can add this code to the view which contains the text field in Swift 3. This will make the text field animate up and down with the keyboard.
private var keyboardIsVisible = false
private var keyboardHeight: CGFloat = 0.0
// MARK: Notifications
private func registerForKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeHidden(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
private func deregisterFromKeyboardNotifications() {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
// MARK: Triggered Functions
#objc private func keyboardWillShow(notification: NSNotification) {
keyboardIsVisible = true
guard let userInfo = notification.userInfo else {
return
}
if let keyboardHeight = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.height {
self.keyboardHeight = keyboardHeight
}
if !textField.isHidden {
if let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber,
let curve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber {
animateHUDWith(duration: duration.doubleValue,
curve: UIViewAnimationCurve(rawValue: curve.intValue) ?? UIViewAnimationCurve.easeInOut,
toLocation: calculateTextFieldCenter())
}
}
}
#objc private func keyboardWillBeHidden(notification: NSNotification) {
keyboardIsVisible = false
if !self.isHidden {
guard let userInfo = notification.userInfo else {
return
}
if let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber,
let curve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber {
animateHUDWith(duration: duration.doubleValue,
curve: UIViewAnimationCurve(rawValue: curve.intValue) ?? UIViewAnimationCurve.easeInOut,
toLocation: calculateTextFieldCenter())
}
}
}
// MARK: - Helpers
private func animateHUDWith(duration: Double, curve: UIViewAnimationCurve, toLocation location: CGPoint) {
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(TimeInterval(duration))
UIView.setAnimationCurve(curve)
textField.center = location
UIView.commitAnimations()
}
private func calculateTextFieldCenter() -> CGPoint {
if !keyboardIsVisible {
return self.center
} else {
let yLocation = (self.view.frame.height - keyboardHeight) / 2
return CGPoint(x: self.center.x, y: yLocation)
}
}