I have a UITableView that contains UITextField in each cell
When I click UITextField in the cell, the keyboard will show and cover my cell. Therefore, I move my cell to the top by using.
- (void)keyboardWasShown:(NSNotification*)aNotification
{
CGPoint scrollPoint = CGPointMake(0, self.activeInputView.frame.origin.y);
[self.tableView setContentOffset:scrollPoint animated:YES];
}
If I use single click on each cell, my application work fine.
However, I use double click on each cell (it mean I tap on it 2 times very quickly), my cell will stop scroll to the top.
In this line
CGPoint scrollPoint = CGPointMake(0, self.activeInputView.frame.origin.y);
can be 2 errors.
Firstly, you have to make sure that inputView that you have captured is the view you want.
Secondly, you should convert fetched point to correct view using appropriate method.
See reference: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/doc/uid/TP40006816-CH3-SW52
For further investigation need more context.
TPKeyboardAvoiding seems to be a solution.
Are you sure that the scrollPoint is correct?
I just created a sample project with a fixed offset and everything works perfectly fine. The table view scrolls upwards after single and double tap on the UITextField of a certain UITableViewCell.
- (void)keyboardDidShow:(NSNotification *)notification {
CGPoint point = CGPointMake(0.0, 30.0);
[self.tableView setContentOffset:point animated:YES];
}
rather then doing code for manually adjusting your tableview frame ,
Let me give you a suggestion, you can use IQKeyboardManager whose pod is available in cocoapods
and it will manage of when clicking on cell's textfield , tableview will automatically scroll.
Try to use third party library IQKeyboardManager it will help you to solve this issue.
https://github.com/hackiftekhar/IQKeyboardManager
Replace following code in your AppDelegate.m file and then you don't need to use scrollview method to handle keyboard :-
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[IQKeyboardManager sharedManager] setEnable:YES];
[self.window makeKeyAndVisible];
return YES;
}
Have you created separate UITableViewCell class ?
I believe as you scroll your UITableView your UI freezes or when you tab twice frequently your UITableView stops responding or taking time to respond.
Here is a working example. You use your scroll view's contentInset and scrollIndicatorInsets properties to avoid the keyboard when it appears. You should register for keyboard notifications. Use the info in the notifications to determine the size of the keyboard--you never know what that will be.
Using content inset is the correct way to handle this for scroll views. If you subsequently need to scroll your editing row into view, use UITableView.scrollToRowAtIndexPath(_:, atScrollPosition:, animated:)
Notice that the does the right thing when the user hides/shows the completions bar.
import UIKit
import CoreGraphics
// our Cell class
class Cell : UITableViewCell
{
// cell reuse identifier for table view
static let Identifier = "Cell"
// the object the cell represents/displays. Could be anything you like
var value:AnyObject? {
didSet {
// update our text field when our cell value is set.
self.textField.text = value as? String
}
}
// use a text field to display our contents, since that allows editing and showing the keyboard
lazy var textField:UITextField = {
let textField = UITextField()
self.contentView.addSubview( textField )
return textField
}()
override func layoutSubviews() {
super.layoutSubviews()
self.textField.frame = contentView.bounds.insetBy(dx: 20, dy: 4 )
}
}
// table view data source class
class DataSource : NSObject, UITableViewDataSource
{
var numberOfRows:Int { return items.count }
let items = [ "Seoul", "São Paulo", "Bombay", "Jakarta", "Karachi", "Moskva", "Istanbul", "Mexico", "Shanghai", "Tokyo", "New" York, "Bangkok", "Beijing", "Delhi", "London", "Hong Kong", "Cairo", "Tehran", "Bogota", "Bandung", "Tianjin", "Lima", "Rio de Janeiro", "Lahore", "Bogor", "Santiago", "St Petersburg", "Shenyang", "Calcutta", "Wuhan" ]
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return numberOfRows
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier( Cell.Identifier ) as? Cell ?? Cell()
cell.value = items[ indexPath.row ]
return cell
}
}
class ViewController : UIViewController
{
override func viewDidLoad() {
super.viewDidLoad()
// register for notifications when the keyboard appears:
NSNotificationCenter.defaultCenter().addObserver( self, selector: "keyboardWillShow:", name: UIKeyboardWillChangeFrameNotification, object: nil)
}
override func viewDidLayoutSubviews() {
tableView.frame = view.bounds
}
lazy var tableView:UITableView = {
let tableView = UITableView()
self.view.addSubview( tableView )
tableView.dataSource = self.dataSource
tableView.delegate = self
return tableView
}()
lazy var dataSource : DataSource = DataSource()
// Handle keyboard frame changes here.
// Use the CGRect stored in the notification to determine what part of the screen the keyboard will cover.
// Adjust our table view's contentInset and scrollIndicatorInsets properties so that the table view content avoids the part of the screen covered by the keyboard
#objc func keyboardWillShow( note:NSNotification )
{
// read the CGRect from the notification (if any)
if let newFrame = (note.userInfo?[ UIKeyboardFrameEndUserInfoKey ] as? NSValue)?.CGRectValue() {
let insets = UIEdgeInsetsMake( 0, 0, newFrame.height, 0 )
tableView.contentInset = insets
tableView.scrollIndicatorInsets = insets
}
}
}
// need to conform to UITableViewDelegate protocol since we are the table view's delegate
extension ViewController : UITableViewDelegate
{
}
// App set up stuff here:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var window:UIWindow? = UIWindow()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
window!.rootViewController = ViewController()
window!.makeKeyAndVisible()
return true
}
}
Related
Sample project can be found at https://github.com/SRowley90/LargeTitleIssueTestiOS
I am trying to position a segmented control below the Large title in an iOS app. I have a UIToolbar which contains the segmented control inside.
When scrolling up the title and toolbar behave as expected.
When scrolling down the navigation bar is correct, but it doesn't push the UITabBar or the UITableView down, meaning the title goes above the segmented control as can be seen in the images below.
I'm pretty sure it's something to do with the constraints I have set, but I can't figure out what.
The TabBar is fixed to the top, left and right.
The TableView is fixed to the bottom, left and right.
The tableView is fixed vertically to the TabBar
I have the position UITabBarDelegate method set:
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
Take the delegation of the tableView somewhere:
tableView.delegate = self
Override the scrollViewDidScroll and update toolbar position appearance (since the real position should not change according to have that nice bounce effect.
extension ViewController: UIScrollViewDelegate {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
var verticalOffset = scrollView.contentOffset.y + defaultNavigationBarHeight
if scrollView.refreshControl?.isRefreshing ?? false {
verticalOffset += 60 // After is refreshing changes its value the toolbar goes 60 points down
print(toolbar.frame.origin.y)
}
if verticalOffset >= 0 {
toolbar.transform = .identity
} else {
toolbar.transform = CGAffineTransform(translationX: 0, y: -verticalOffset)
}
}
}
You can use the following check before applying transformation to make it more reliable and natural to default iOS style:
if #available(iOS 11.0, *) {
guard let navigationController = navigationController else { return }
guard navigationController.navigationBar.prefersLargeTitles else { return }
guard navigationController.navigationItem.largeTitleDisplayMode != .never else { return }
}
Using UIScrollViewDelegate didn't work well with CollectionView and toolbar for me. So, I did:
final class CollectionViewController: UICollectionViewController {
private var observesBag: [NSKeyValueObservation] = []
private let toolbar = UIToolbar()
override func viewDidLoad() {
super.viewDidLoad()
let statusBarHeight = UIApplication.shared.statusBarFrame.height
let navigationBarHeight = navigationController?.navigationBar.frame.height ?? 0
let defaultNavigationBarHeight = statusBarHeight + navigationBarHeight
let observation = navigationController!
.navigationBar
.observe(\.center, options: NSKeyValueObservingOptions.new) { [weak self] navBar, _ in
guard let self = self else { return }
let newNavigatonBarHeight = navBar.frame.height + statusBarHeight
let yTranslantion = newNavigatonBarHeight - defaultNavigationBarHeight
if yTranslantion > 0 {
self.toolbar.transform = CGAffineTransform(
translationX: 0,
y: yTranslantion
)
} else {
self.toolbar.transform = .identity
}
}
observesBag.append(observation)
}
}
Observe the "center" of the navigationBar for changes and then translate the toolbar in the y-axis.
Even though it worked fine when I tried to use this solution with UIRefreshControl and Large Titles it didn't work well.
I set up the refresh control like:
private func setupRefreshControl() {
let refreshControl = UIRefreshControl()
self.webView.scrollView.refreshControl = refreshControl
}
the height of the UINavigationBar is changed after the complete refresh triggers.
For an app that I'm developing, I'm using a tableview. My tableview, has of course a tableviewcell. In that tableviewcell I'm adding a view programmatically (so nothing in storyboard). It's a view where I draw some lines and text and that becomes a messagebubble. If the messagebubble is seen by the other user you sent it too , a line of the bubble will go open.
So I have the animation function inside the class of that UIView (sendbubble.swift)
Now, it already checks if it is read or not and it opens the right bubble. But normally it should animate (the line that goes open should rotate) in 0.6 seconds. But it animates instantly. So my question is, how do I animate it with a duration?
I would also prefer to still call it in my custom UIView class (sendbubble.swift) . Maybe I need code in my function to check if the cell is presented on my iphone?
Thanks in advance!
func openMessage() {
UIView.animate(withDuration: 0.6, delay: 0.0, options: [], animations: {
var t = CATransform3DIdentity;
t = CATransform3DMakeRotation(CGFloat(3 * Float.pi / 4), 0, 0, 1)
self.moveableLineLayer.transform = t;
}, completion:{(finished:Bool) in })
}
First you need to grab the cell.
Get the indexPath of that cell where you need to show open bubble.
Get the cell from tableView.cellForRowAt(at:indexPath)
We will now have access to that bubble view now you can animate using the same function func openMessage()
Any question? comment.
You need to implement the UITableViewDelegate method tableView(_:willDisplay:forRowAt:) https://developer.apple.com/reference/uikit/uitableviewdelegate/1614883-tableview
That is triggered when the cell is about to be drawn - not when it is created. You will also need to store a state in your model that says if the animation has occurred, otherwise it will happen every time the cell comes back into view.
EXAMPLE
In the view controller (pseudo code)
class CustomViewController: UITableViewDelegate {
//where ever you define your tableview
var tableView:UITableView
tableView.delegate = self
var dataSource //some array that is defining your cells - each object has a property call hasAnimated
func tableView(_ tableView: UITableView, willDisplay cell: ITableViewCell,
forRowAt indexPath: IndexPath) {
if let cell = cell as? CustomTableViewCell, dataSource[indexPath.row].hasAnimated == false {
dataSource[indexPath.row].hasAnimated = true
cell.openMessage()
}
}
}
class CustomTableViewCell: UITableViewCell {
func openMessage() { //your method
}
}
In my app, I have a summary page which will display the summary of payers and recipients list of each payer. That is, the summary page will have a list in the left side and clicking on each list will show its details on the right side(iPad). In order to re-use the same code for iPhone version too, I have it as separate view controllers. That is, I have a base ViewController(SummaryViewController). In this i subview the ViewController of the list(SummaryListViewController) and ViewController of the details(SummaryDetailViewController). Now, when the base view controller SummaryViewController loads, i subview the list and detail view controllers like this
//ListView is a view in the base ViewController to which i subview the list ViewController
let listViewController = SummaryListViewController(nibName:"SummaryListViewController", bundle: nil)
addChildViewController(listViewController)
listViewController.view.frame = CGRect(x: 0, y: 0, width: self.ListView.frame.size.width, height: self.ListView.frame.size.height)
ListView.addSubview(listViewController.view)
listViewController.didMove(toParentViewController: self)
//DetailView is a view in the base ViewController to which i subview the Detail ViewController
let detailViewController = SummaryDetailViewController(nibName: (UIDevice.current.userInterfaceIdiom == .pad ? "SummaryDetailViewController" : "SummaryDetailViewController_iPhone"), bundle: nil)
addChildViewController(detailViewController)
DetailView.frame = CGRect(x: DetailView.frame.origin.x, y: DetailView.frame.origin.y, width: self.DetailView.frame.size.width, height: self.DetailView.frame.size.height)
DetailView.addSubview(detailViewController.view)
detailViewController.didMove(toParentViewController: self)
Now, the prob is, I have to call a method in the SummaryDetailViewController from the tableView-didSelectRow of SummaryListViewController which will update the UI elements according to the data i send.
I have tried the following things to achieve this,
I tried using addObserver in NotificationCenter. When i click on the list, i added an observer. And this observer triggers the method in the detailViewController that will update the UI elements. But this doesn't work well all the time. When i come to Summary page, go back and if i come again to summary page and do the same, the observer is called twice even after removing the observer in ViewDidDisapper. And also i learnt in few websites that NotificationCenter should not be used for this kind of a situation.
Second, Am trying to use protocols. I thought i would write a protocol in the SummaryListViewController
protocol SummaryDetailProtocol {
func setSummaryDetails()
}
class SummaryListViewController: UIViewController
{
var summaryDetailsDelegate : SummaryDetailProtocol?
func delegateFromSummaryDetails(delegate: SummaryDetailProtocol)
{
self.summaryDetailsDelegate = delegate
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
self.summaryDetailsDelegate?.setSummaryDetails()
}
func delegateFromSummaryDetails(delegate: SummaryDetailProtocol)
{
self.summaryDetailsDelegate = delegate
}
}
Now in the ViewDidLoad of the SummaryDetailViewController, I would like to send the reference of the delegate to the listViewController so that the listViewController can call the setSummaryDetails method.
override func viewDidLoad()
{
super.viewDidLoad()
sendDelegateReferenceToListPage()
}
func sendDelegateReferenceToListPage()
{
let summaryListObj = SummaryListView()
//This is where the error occurs. It throws the error since i try to cast SummaryDetailViewController to parameter of type SummaryDetailProtocol of SummaryListViewController
summaryListObj.delegateFromSummaryDetails(delegate: self as! SummaryDetailProtocol)
}
Can anyone help me to get out of this
Protocol solution is the best. You can use like this
SummaryListViewController file:
protocol SummaryDetailProtocol {
func setSummaryDetails()
}
class SummaryListViewController: UIViewController
{
var summaryDetailsDelegate : SummaryDetailProtocol?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
self.summaryDetailsDelegate?.setSummaryDetails()//use here
}
}
SummaryDetailViewController file:
class SummaryDetailViewController: UIViewController,SummaryDetailProtocol
{
internal func setSummaryDetails() {
//return whatever you want
}
}
And finally set SummaryListViewController delegate to SummaryDetailViewController:
let detailViewController = SummaryDetailViewController(nibName: (UIDevice.current.userInterfaceIdiom == .pad ? "SummaryDetailViewController" : "SummaryDetailViewController_iPhone"), bundle: nil)
addChildViewController(detailViewController)
DetailView.frame = CGRect(x: DetailView.frame.origin.x, y: DetailView.frame.origin.y, width: self.DetailView.frame.size.width, height: self.DetailView.frame.size.height)
DetailView.addSubview(detailViewController.view)
detailViewController.didMove(toParentViewController: self)
let listViewController = SummaryListViewController(nibName:"SummaryListViewController", bundle: nil)
addChildViewController(listViewController)
listViewController.view.frame = CGRect(x: 0, y: 0, width: self.ListView.frame.size.width, height: self.ListView.frame.size.height)
ListView.addSubview(listViewController.view)
listViewController.didMove(toParentViewController: self)
listViewController.summaryDetailsDelegate = detailViewController // set delegate detail view controller
I'm trying to implement 6 lines high description label and I want it to be focusable. Ideally that would mean extending UILabel class to make a custom component. I tried that by implementing canBecomeFocused and didUpdateFocusInContext but my UILabel doesn't seem to get any focus.
I also tried replacing UILabel with UIButton, but buttons aren't really optimised for this sort of thing. Also that would mean I'd need to change buttonType on focus from custom to plain.. and buttonType seems to be a ready-only property.
In reality I'd like to have exact same text label implemented by Apple in Apple TV Movies app. For movie description they have a text label that displays a few lines of text and a "more". When focused it looks like a button (shadows around) and changed background color. When tapped - it opens up a modal window with entire movie description.
Any suggestions? Or maybe someone has already implemented this custom control for tvOS? Or event better - there is a component from Apple that does this and I'm missing something.
P.S: Swift solution would be welcome :)
Ok, answering my own question :)
So it appears that some some views are "focusable" on tvOS out-of-the-box, and other have to be instructed to do so.
I finally ended up using UITextView, which has a selectable property, but if not one of these focusable views by default. Editing of TextView has to be disabled to make it look like UILabel. Also, currently there is a bug which prevents you from using selectable property from Interface Builder but works from code.
Naturally, canBecomeFocused() and didUpdateFocusInContext had to be implemented too. You'll also need to pass a UIViewController because UITextView is not capable of presenting a modal view controller. Bellow is what I ended up creating.
class FocusableText: UITextView {
var data: String?
var parentView: UIViewController?
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: "tapped:")
tap.allowedPressTypes = [NSNumber(integer: UIPressType.Select.rawValue)]
self.addGestureRecognizer(tap)
}
func tapped(gesture: UITapGestureRecognizer) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let descriptionView = storyboard.instantiateViewControllerWithIdentifier("descriptionView") as? DescriptionViewController {
if let view = parentView {
if let show = show {
descriptionView.descriptionText = self.data
view.modalPresentationStyle = UIModalPresentationStyle.OverFullScreen
view.presentViewController(descriptionView, animated: true, completion: nil)
}
}
}
}
override func canBecomeFocused() -> Bool {
return true
}
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
if context.nextFocusedView == self {
coordinator.addCoordinatedAnimations({ () -> Void in
self.layer.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.2).CGColor
}, completion: nil)
} else if context.previouslyFocusedView == self {
coordinator.addCoordinatedAnimations({ () -> Void in
self.layer.backgroundColor = UIColor.clearColor().CGColor
}, completion: nil)
}
}
}
As for making a UILabel focusable:
class MyLabel: UILabel {
override var canBecomeFocused: Bool {
return true
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocus(in: context, with: coordinator)
backgroundColor = context.nextFocusedView == self ? .blue:.red
}
}
IMPORTANT!!!
As stated on the apple developer portal:
The value of this property is true if the view can become focused; false otherwise.
By default, the value of this property is false. This property informs the focus engine if a view is capable of being focused. Sometimes even if a view returns true, a view may not be focusable for the following reasons:
The view is hidden.
The view has alpha set to 0.
The view has userInteractionEnabled set to false.
The view is not currently in the view hierarchy.
Use a collection view with just one cell and add transform to cell and change cell background color in didUpdateFocusInContext when focus moves to cell.
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
coordinator.addCoordinatedAnimations({
if self.focused {
self.transform = CGAffineTransformMakeScale(1.01, 1.01)
self.backgroundColor = UIColor.whiteColor()
self.textLabel.textColor = .blackColor()
}
else {
self.transform = CGAffineTransformMakeScale(1, 1)
self.backgroundColor = UIColor.clearColor()
self.textLabel.textColor = .whiteColor()
}
}, completion: nil)
}
As an additional step you could try to extract the color of the image if you are using the image as background like iTunes and use that for Visual effect view behind the cell.
Also you can apply transform to the collectionView in the video controller to make it look like in focus
You can use system button, and set the background image in storyboard to an image that contains the color you would like
I'm trying to get the height of the iOS keyboard. I've gone through and used the method involving subscribing to a notification such as detailed here:
https://gist.github.com/philipmcdermott/5183731
- (void)viewDidAppear:(BOOL) animated {
[super viewDidAppear:animated];
// Register notification when the keyboard will be show
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
// Register notification when the keyboard will be hide
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)keyboardWillShow:(NSNotification *)notification {
CGRect keyboardBounds;
[[notification.userInfo valueForKey:UIKeyboardFrameBeginUserInfoKey] getValue:&keyboardBounds];
// Do something with keyboard height
}
- (void)keyboardWillHide:(NSNotification *)notification {
CGRect keyboardBounds;
[[notification.userInfo valueForKey:UIKeyboardFrameBeginUserInfoKey] getValue:&keyboardBounds];
// Do something with keyboard height
}
This works fine for when the user actually displays the keyboard.
My problem: I have another view, let's call it micView, that may be presented before the keyboard appears. The user may choose to use the microphone before typing. I would like the micView to be the same height as the keyboard, which is why I need the keyboard's height, but I need it before the keyboard was forced to appear. Thus the UIKeyboardWillShowNotification is not reached before I need to read the value of the height.
My question is: how do I get the height of the keyboard through Notifications, or some other method without ever having the keyboard appear.
I considered explicitly forcing the keyboard to appear in viewDidLoad, so that I can set an instance variable to that value, then hiding it and getting rid of the animations for both things. But is that really the only way to do that?
This Swift class provides a turn-key solution that manages all the necessary notifications and initializations, letting you simply call a class method and have returned the keyboard size or height.
Calling from Swift:
let keyboardHeight = KeyboardService.keyboardHeight()
let keyboardSize = KeyboardService.keyboardSize()
Calling from Objective-C:
CGFloat keyboardHeight = [KeyboardService keyboardHeight];
CGRect keyboardSize = [KeyboardService keyboardSize];
If wanting to use this for initial view layout, call this from the viewWillAppear method of a class where you want the keyboard height or size before the keyboard appears. It should not be called in viewDidLoad, as a correct value relies on your views having been laid out. You can then set an autolayout constraint constant with the value returned from the KeyboardService, or use the value in other ways. For instance, you might want to obtain the keyboard height in prepareForSegue to assist in setting a value associated with the contents of a containerView being populated via an embed segue.
Note re safe area, keyboard height, and iPhone X:
The value for keyboard height returns the full height of the keyboard, which on the iPhone X extends to the edge of the screen itself, not just to the safe area inset. Therefore, if setting an auto layout constraint value with the returned value, you should attach that constraint to the superview bottom edge, not to the safe area.
Note re hardware keyboard in Simulator:
When a hardware keyboard is attached, this code will provide the on-screen height of that hardware keyboard, that is, no height. This state does need to be accounted for, of course, as this simulates what will occur if you have a hardware keyboard attached to an actual device. Therefore, your layout that is expecting a keyboard height needs to respond appropriately to a keyboard height of zero.
KeyboardService class:
As usual, if calling from Objective-C, you simply need to import the app's Swift bridging header MyApp-Swift.h in your Objective-C class.
import UIKit
class KeyboardService: NSObject {
static var serviceSingleton = KeyboardService()
var measuredSize: CGRect = CGRect.zero
#objc class func keyboardHeight() -> CGFloat {
let keyboardSize = KeyboardService.keyboardSize()
return keyboardSize.size.height
}
#objc class func keyboardSize() -> CGRect {
return serviceSingleton.measuredSize
}
private func observeKeyboardNotifications() {
let center = NotificationCenter.default
center.addObserver(self, selector: #selector(self.keyboardChange), name: .UIKeyboardDidShow, object: nil)
}
private func observeKeyboard() {
let field = UITextField()
UIApplication.shared.windows.first?.addSubview(field)
field.becomeFirstResponder()
field.resignFirstResponder()
field.removeFromSuperview()
}
#objc private func keyboardChange(_ notification: Notification) {
guard measuredSize == CGRect.zero, let info = notification.userInfo,
let value = info[UIKeyboardFrameEndUserInfoKey] as? NSValue
else { return }
measuredSize = value.cgRectValue
}
override init() {
super.init()
observeKeyboardNotifications()
observeKeyboard()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
Head nod:
The observeKeyboard method here based on the original approach outlined by Peres in the Objective-C answer to this question.
A quick solution that you could use, is the same one used when you want to cache the keyboard (the first time you show it, you get a slight delay...). The library is here. The interesting bits:
[[[[UIApplication sharedApplication] windows] lastObject] addSubview:field];
[field becomeFirstResponder];
[field resignFirstResponder];
[field removeFromSuperview];
So basically is showing it and then hiding it. You could listen for notifications and just get the height without actually seeing it. Bonus: you get to cache it. :)
Looks like this solution did stop working.
I modified it:
adding a callback to know when the notification arrives with the real height,
moving the textfield to another window to avoid showing it, and
setting a timeout for the case when is used in the simulator and the software keyboard is setted up to now show.
Using Swift 4:
import UIKit
public class KeyboardSize {
private static var sharedInstance: KeyboardSize?
private static var measuredSize: CGRect = CGRect.zero
private var addedWindow: UIWindow
private var textfield = UITextField()
private var keyboardHeightKnownCallback: () -> Void = {}
private var simulatorTimeout: Timer?
public class func setup(_ callback: #escaping () -> Void) {
guard measuredSize == CGRect.zero, sharedInstance == nil else {
return
}
sharedInstance = KeyboardSize()
sharedInstance?.keyboardHeightKnownCallback = callback
}
private init() {
addedWindow = UIWindow(frame: UIScreen.main.bounds)
addedWindow.rootViewController = UIViewController()
addedWindow.addSubview(textfield)
observeKeyboardNotifications()
observeKeyboard()
}
public class func height() -> CGFloat {
return measuredSize.height
}
private func observeKeyboardNotifications() {
let center = NotificationCenter.default
center.addObserver(self, selector: #selector(self.keyboardChange), name: UIResponder.keyboardDidShowNotification, object: nil)
}
private func observeKeyboard() {
let currentWindow = UIApplication.shared.keyWindow
addedWindow.makeKeyAndVisible()
textfield.becomeFirstResponder()
currentWindow?.makeKeyAndVisible()
setupTimeoutForSimulator()
}
#objc private func keyboardChange(_ notification: Notification) {
textfield.resignFirstResponder()
textfield.removeFromSuperview()
guard KeyboardSize.measuredSize == CGRect.zero, let info = notification.userInfo,
let value = info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
else { return }
saveKeyboardSize(value.cgRectValue)
}
private func saveKeyboardSize(_ size: CGRect) {
cancelSimulatorTimeout()
KeyboardSize.measuredSize = size
keyboardHeightKnownCallback()
KeyboardSize.sharedInstance = nil
}
private func setupTimeoutForSimulator() {
#if targetEnvironment(simulator)
let timeout = 2.0
simulatorTimeout = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false, block: { (_) in
print(" KeyboardSize")
print(" .keyboardDidShowNotification did not arrive after \(timeout) seconds.")
print(" Please check \"Toogle Software Keyboard\" on the simulator (or press cmd+k in the simulator) and relauch your app.")
print(" A keyboard height of 0 will be used by default.")
self.saveKeyboardSize(CGRect.zero)
})
#endif
}
private func cancelSimulatorTimeout() {
simulatorTimeout?.invalidate()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
Is used in the following way:
let splashVC = some VC to show in the key window during the app setup (just after the didFinishLaunching with options)
window.rootViewController = splashVC
KeyboardSize.setup() { [unowned self] in
let kbHeight = KeyboardSize.height() // != 0 :)
// continue loading another things or presenting the onboarding or the auth
}
For iOS 14.0, I noticed that this solution stopped working on around the 10th call as NotificationCenter stopped broadcasting keyboardChange notification. I was not able to fully figure out why that was happening.
So, I tweaked the solution to make KeyboardSize a singleton and added a method updateKeyboardHeight() as such:
static let shared = KeyboardSize()
/**
Height of keyboard after the class is initialized
*/
private(set) var keyboardHeight: CGFloat = 0.0
private override init() {
super.init()
observeKeyboardNotifications()
observeKeyboard()
}
func updateKeyboardHeight() {
observeKeyboardNotifications()
observeKeyboard()
}
and used it as
KeyboardSize.shared.updateKeyboardHeight()
let heightOfKeyboard = KeyboardSize.shared.keyboardHeight