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))
}
}
Related
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:)
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)")
}
}
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)```
I'm trying to have every object in the view be shifted up when a text field is present, and to reduce amount of code, I tried making the modifiers into a modifier class, but the issue is when the y value gets changed for the keyboard handlers, it doesn't move. This exact code works if you don't use the separate function, so I don't know what's wrong.
I've tried making the value of the y a state in the modifier, but it still doesn't update - and like I said, it works if it's not in the custom modifier.
ContentView.swift
struct ContentView: View {
#State private var name = Array<String>.init(repeating: "", count: 3)
#ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: 1)
var body: some View {
Background {
VStack {
Group {
Text("Some filler text").font(.largeTitle)
Text("Some filler text").font(.largeTitle)
}
TextField("enter text #1", text: self.$name[0])
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("enter text #2", text: self.$name[1])
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("enter text #3", text: self.$name[2])
.textFieldStyle(RoundedBorderTextFieldStyle())
.background(GeometryGetter(rect: self.$kGuardian.rects[0]))
}
}.modifier(BetterTextField(kGuardian: kGuardian, slide: kGuardian.slide))
}
}
KeyboardModifications.swift
struct GeometryGetter: View {
#Binding var rect: CGRect
var body: some View {
GeometryReader { geometry in
Group { () -> AnyView in
DispatchQueue.main.async {
self.rect = geometry.frame(in: .global)
}
return AnyView(Color.clear)
}
}
}
}
final class KeyboardGuardian: ObservableObject {
public var rects: Array<CGRect>
public var keyboardRect: CGRect = CGRect()
// keyboardWillShow notification may be posted repeatedly,
// this flag makes sure we only act once per keyboard appearance
public var keyboardIsHidden = true
#Published var slide: CGFloat = 0
var appearenceDuration : Double = 0
var showField: Int = 0 {
didSet {
updateSlide()
}
}
init(textFieldCount: Int) {
self.rects = Array<CGRect>(repeating: CGRect(), count: textFieldCount)
NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc func keyBoardWillShow(notification: Notification) {
guard let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else {return}
appearenceDuration = duration
if keyboardIsHidden {
keyboardIsHidden = false
if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
keyboardRect = rect
updateSlide()
}
}
}
#objc func keyBoardWillHide(notification: Notification) {
keyboardIsHidden = true
updateSlide()
}
func updateSlide() {
if keyboardIsHidden {
slide = 0
} else {
let tfRect = self.rects[self.showField]
let diff = keyboardRect.minY - tfRect.maxY
if diff > 0 {
slide += diff
} else {
slide += min(diff, 0)
}
}
}
}
struct Background<Content: View>: View {
private var content: Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content()
}
var body: some View {
Color.white
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.overlay(content)
}
}
The custom modifier class:
struct BetterTextField: ViewModifier {
public var kGuardian : KeyboardGuardian
#State public var slide : CGFloat
func body(content: Content) -> some View {
content
.offset(y: slide).animation(.easeIn(duration: kGuardian.appearenceDuration))
.onTapGesture {
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow!.endEditing(true)
}
}
}
What should happen is the entire screen should shift up with the keyboard, but it stays as it is. I've been trying things for around 2 hours to no avail.
I guess your slide variable is not updated. Try with the following change code in BetterTextField modifier:
struct BetterTextField: ViewModifier {
#ObservedObject var kGuardian : KeyboardGuardian
func body(content: Content) -> some View {
content
.offset(y: kGuardian.slide).animation(.easeIn(duration: kGuardian.appearenceDuration))
.onTapGesture {
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow!.endEditing(true)
}
}
}
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)
}
}