From 317c66dcd877dd08368ec194476a6f965d2d6269 Mon Sep 17 00:00:00 2001 From: toshi0383 Date: Fri, 28 Oct 2022 08:08:28 +0900 Subject: [PATCH 1/3] fix: support formSheet modal use case --- .../project.pbxproj | 4 + .../Base.lproj/Main.storyboard | 135 ++++++++++++++++-- .../FormViewController.swift | 43 ++++++ .../ViewController.swift | 18 ++- .../KeyboardLayoutGuide.swift | 68 ++++++--- 5 files changed, 240 insertions(+), 28 deletions(-) create mode 100644 KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/FormViewController.swift diff --git a/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample.xcodeproj/project.pbxproj b/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample.xcodeproj/project.pbxproj index 084bdaf..6071d5a 100644 --- a/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample.xcodeproj/project.pbxproj +++ b/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 50F1DDE31FCC267900600110 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F1DDE21FCC267900600110 /* Extensions.swift */; }; + 8A6591BB290B479B0093D636 /* FormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A6591BA290B479B0093D636 /* FormViewController.swift */; }; 9902DE341FBB2659009E0D48 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9902DE331FBB2659009E0D48 /* AppDelegate.swift */; }; 9902DE361FBB2659009E0D48 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9902DE351FBB2659009E0D48 /* ViewController.swift */; }; 9902DE391FBB2659009E0D48 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9902DE371FBB2659009E0D48 /* Main.storyboard */; }; @@ -31,6 +32,7 @@ /* Begin PBXFileReference section */ 50F1DDE21FCC267900600110 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 8A6591BA290B479B0093D636 /* FormViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormViewController.swift; sourceTree = ""; }; 9902DE301FBB2659009E0D48 /* KeyboardLayoutGuideExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeyboardLayoutGuideExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9902DE331FBB2659009E0D48 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 9902DE351FBB2659009E0D48 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -76,6 +78,7 @@ children = ( 9902DE331FBB2659009E0D48 /* AppDelegate.swift */, 9902DE351FBB2659009E0D48 /* ViewController.swift */, + 8A6591BA290B479B0093D636 /* FormViewController.swift */, 50F1DDE21FCC267900600110 /* Extensions.swift */, 9902DE371FBB2659009E0D48 /* Main.storyboard */, 9902DE3A1FBB2659009E0D48 /* Assets.xcassets */, @@ -172,6 +175,7 @@ files = ( 9902DE361FBB2659009E0D48 /* ViewController.swift in Sources */, 9902DE341FBB2659009E0D48 /* AppDelegate.swift in Sources */, + 8A6591BB290B479B0093D636 /* FormViewController.swift in Sources */, 50F1DDE31FCC267900600110 /* Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/Base.lproj/Main.storyboard b/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/Base.lproj/Main.storyboard index 9b0be46..2494dc9 100644 --- a/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/Base.lproj/Main.storyboard +++ b/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/Base.lproj/Main.storyboard @@ -1,11 +1,11 @@ - - - - + + - + + + @@ -18,11 +18,10 @@ - + - @@ -41,18 +40,28 @@ + + + + - @@ -62,5 +71,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/FormViewController.swift b/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/FormViewController.swift new file mode 100644 index 0000000..4123154 --- /dev/null +++ b/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/FormViewController.swift @@ -0,0 +1,43 @@ +import KeyboardLayoutGuide +import UIKit + +protocol FormViewControllerDelegate: AnyObject { + func formViewControllerWillDismiss() +} + +final class FormViewController: UIViewController { + + @IBOutlet weak var vStack: UIStackView! + @IBOutlet weak var textField: UITextField! + @IBOutlet weak var elementHeight: NSLayoutConstraint! + private weak var delegate: FormViewControllerDelegate? + + static func make(delegate: FormViewControllerDelegate) -> FormViewController { + let vc = UIStoryboard(name: "Main", bundle: nil) + .instantiateViewController(withIdentifier: "FormViewController") as! FormViewController + vc.delegate = delegate + vc.modalPresentationStyle = .formSheet + vc.preferredContentSize = CGSize(width: 500, height: 600) + return vc + } + + override func viewDidLoad() { + super.viewDidLoad() + + Keyboard.shared.presentedViewController = self + vStack.bottomAnchor.constraint(lessThanOrEqualTo: view.keyboardLayoutGuideNoSafeArea.topAnchor).isActive = true + + view.addGestureRecognizer(UITapGestureRecognizer(target: textField, action: #selector(resignFirstResponder))) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + delegate?.formViewControllerWillDismiss() + } + + @IBAction func handlePan(_ pan: UIPanGestureRecognizer) { + elementHeight.constant = 50 + pan.translation(in: pan.view).y + } + +} diff --git a/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/ViewController.swift b/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/ViewController.swift index 468b120..f6b55c5 100644 --- a/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/ViewController.swift +++ b/KeyboardLayoutGuideExample/KeyboardLayoutGuideExample/ViewController.swift @@ -12,14 +12,28 @@ import KeyboardLayoutGuide class ViewController: UIViewController { @IBOutlet weak var button: UIButton! - + private var keyboardConstraint: NSLayoutConstraint? + override func viewDidLoad() { super.viewDidLoad() // Constrain your button to the keyboardLayoutGuide's top Anchor the way you would do natively :) - button.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor).isActive = true + keyboardConstraint = button.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor) + keyboardConstraint?.isActive = true // Opt out of safe area if needed. // button.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuideNoSafeArea.topAnchor).isActive = true } + + @IBAction func showFormSheet(_ sender: Any) { + keyboardConstraint?.isActive = false + let vc = FormViewController.make(delegate: self) + present(vc, animated: true) + } +} + +extension ViewController: FormViewControllerDelegate { + func formViewControllerWillDismiss() { + keyboardConstraint?.isActive = true + } } diff --git a/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift b/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift index fa36f2d..b31e757 100644 --- a/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift +++ b/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift @@ -8,9 +8,13 @@ import UIKit -internal class Keyboard { - static let shared = Keyboard() +public class Keyboard { + public static let shared = Keyboard() var currentHeight: CGFloat = 0 + + /// If you do know you're presenting a modal in either `.formSheet` style, + /// set this field to fix unexpected behavior of this Library. + public weak var presentedViewController: UIViewController? } extension UIView { @@ -47,7 +51,7 @@ extension UIView { open class KeyboardLayoutGuide: UILayoutGuide { public var usesSafeArea = true { didSet { - updateBottomAnchor() + updateButtomAnchor() } } @@ -63,15 +67,14 @@ open class KeyboardLayoutGuide: UILayoutGuide { // Observe keyboardWillChangeFrame notifications notificationCenter.addObserver( self, - selector: #selector(adjustKeyboard(_:)), + selector: #selector(keyboardWillChangeFrame(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil ) - // Observe keyboardWillHide notifications notificationCenter.addObserver( self, - selector: #selector(adjustKeyboard(_:)), - name: UIResponder.keyboardWillHideNotification, + selector: #selector(keyboardDidChangeFrame(_:)), + name: UIResponder.keyboardDidChangeFrameNotification, object: nil ) } @@ -85,10 +88,10 @@ open class KeyboardLayoutGuide: UILayoutGuide { rightAnchor.constraint(equalTo: view.rightAnchor), ] ) - updateBottomAnchor() + updateButtomAnchor() } - func updateBottomAnchor() { + func updateButtomAnchor() { if let bottomConstraint = bottomConstraint { bottomConstraint.isActive = false } @@ -107,7 +110,31 @@ open class KeyboardLayoutGuide: UILayoutGuide { } @objc - private func adjustKeyboard(_ note: Notification) { + private func keyboardDidChangeFrame(_ note: Notification) { + guard Keyboard.shared.presentedViewController != nil else { + return + } + + if var height = note.keyboardHeight, let duration = note.animationDuration { + if #available(iOS 11.0, *), usesSafeArea, height > 0, let bottom = owningView?.safeAreaInsets.bottom { + height -= bottom + } + heightConstraint?.constant = height + if duration > 0.0 { + UIView.animate(withDuration: 0.2) { + self.animate(note) + } + } + Keyboard.shared.currentHeight = height + } + } + + @objc + private func keyboardWillChangeFrame(_ note: Notification) { + guard Keyboard.shared.presentedViewController == nil else { + return + } + if var height = note.keyboardHeight, let duration = note.animationDuration { if #available(iOS 11.0, *), usesSafeArea, height > 0, let bottom = owningView?.safeAreaInsets.bottom { height -= bottom @@ -149,15 +176,20 @@ extension Notification { guard let keyboardFrame = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return nil } - - if name == UIResponder.keyboardWillHideNotification { - return 0.0 - } else { - // Weirdly enough UIKeyboardFrameEndUserInfoKey doesn't have the same behaviour - // in ios 10 or iOS 11 so we can't rely on v.cgRectValue.width - let screenHeight = UIApplication.shared.keyWindow?.bounds.height ?? UIScreen.main.bounds.height - return screenHeight - keyboardFrame.cgRectValue.minY + + let keyboardMinY = keyboardFrame.cgRectValue.minY + + // Weirdly enough UIKeyboardFrameEndUserInfoKey doesn't have the same behaviour + // in ios 10 or iOS 11 so we can't rely on v.cgRectValue.width + if let pvc = Keyboard.shared.presentedViewController, + let w = UIApplication.shared.keyWindow { + + return w.convert(pvc.view.frame, from: pvc.view.superview).maxY - keyboardMinY } + + let screenHeight: CGFloat = UIApplication.shared.keyWindow?.bounds.height ?? UIScreen.main.bounds.height + + return screenHeight - keyboardMinY } var animationDuration: CGFloat? { From bfc242abed0a8dc67d9e67c7a6f9e0ae9ec94863 Mon Sep 17 00:00:00 2001 From: toshi0383 Date: Fri, 28 Oct 2022 09:50:15 +0900 Subject: [PATCH 2/3] fix: conflict --- .../KeyboardLayoutGuide.swift | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift b/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift index b31e757..d12a2c4 100644 --- a/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift +++ b/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift @@ -51,7 +51,7 @@ extension UIView { open class KeyboardLayoutGuide: UILayoutGuide { public var usesSafeArea = true { didSet { - updateButtomAnchor() + updateBottomAnchor() } } @@ -71,6 +71,12 @@ open class KeyboardLayoutGuide: UILayoutGuide { name: UIResponder.keyboardWillChangeFrameNotification, object: nil ) + notificationCenter.addObserver( + self, + selector: #selector(keyboardWillChangeFrame(_:)), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) notificationCenter.addObserver( self, selector: #selector(keyboardDidChangeFrame(_:)), @@ -88,10 +94,10 @@ open class KeyboardLayoutGuide: UILayoutGuide { rightAnchor.constraint(equalTo: view.rightAnchor), ] ) - updateButtomAnchor() + updateBottomAnchor() } - func updateButtomAnchor() { + func updateBottomAnchor() { if let bottomConstraint = bottomConstraint { bottomConstraint.isActive = false } @@ -177,6 +183,10 @@ extension Notification { return nil } + if name == UIResponder.keyboardWillHideNotification { + return 0.0 + } + let keyboardMinY = keyboardFrame.cgRectValue.minY // Weirdly enough UIKeyboardFrameEndUserInfoKey doesn't have the same behaviour From 2f7b392e96bed6750e8b5e0101e85fe6406eda79 Mon Sep 17 00:00:00 2001 From: toshi0383 Date: Fri, 9 Dec 2022 17:12:01 +0900 Subject: [PATCH 3/3] fix: ignore floating keyboard --- .../KeyboardLayoutGuide.swift | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift b/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift index d12a2c4..ef585da 100644 --- a/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift +++ b/Sources/KeyboardLayoutGuide/KeyboardLayoutGuide.swift @@ -179,7 +179,7 @@ extension UILayoutGuide { extension Notification { var keyboardHeight: CGFloat? { - guard let keyboardFrame = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { + guard let keyboardEndFrame = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return nil } @@ -187,7 +187,21 @@ extension Notification { return 0.0 } - let keyboardMinY = keyboardFrame.cgRectValue.minY + let keyboardMinY = keyboardEndFrame.cgRectValue.minY + + let isLikelyFloating: Bool = { + if keyboardMinY == 0 { return true } + + guard let keyboardBeginFrame = userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue else { + return false + } + + return keyboardBeginFrame.cgRectValue.minY == 0 + }() + + if isLikelyFloating { + return nil + } // Weirdly enough UIKeyboardFrameEndUserInfoKey doesn't have the same behaviour // in ios 10 or iOS 11 so we can't rely on v.cgRectValue.width